Remove non-intuitive unpacking requirement in select-builder-pgsql, fix transactions

Breaking change. Changes [ [ 'id', 1, 2, 3 ] ] set syntax to [ [ 'id', [ 1, 2, 3 ] ] ].
master
Vitaliy Filippov 2021-09-12 22:32:11 +03:00
parent 09c91c6bd1
commit 87bbc60ffd
2 changed files with 122 additions and 73 deletions

View File

@ -25,15 +25,15 @@ export type Fields = string | string[] | { [field: string]: string };
export type Value = string | number | null | {}; export type Value = string | number | null | {};
// - string: 'a=b AND c=d' // - string: 'a=b AND c=d'
// - array: [ 'a=b', [ 'a=? or b=?', 1, 2 ], [ 'a', 1, 2 ], [ '(a, b)', [ 1, 2 ], [ 3, 4 ] ] ] // - array: [ 'a=b', [ 'a=? or b=?', 1, 2 ], [ 'a', [ 1, 2 ] ], [ '(a, b)', [ [ 1, 2 ], [ 3, 4 ] ] ] ]
// - object: { a: 1, b: [ 1, 2 ], 'a = b': [], '(a, b)': [ [ 1, 2 ], [ 2, 3 ] ], 'c=? or d=?': [ 2, 3 ] } // - object: { a: 1, b: [ 1, 2 ], 'a = b': [], '(a, b)': [ [ 1, 2 ], [ 2, 3 ] ], 'c=? or d=?': [ 2, 3 ] }
// - key does not contain '?', value is a scalar or non-empty array => (key IN ...) // - key does not contain '?', value is a scalar or non-empty array => (key IN ...)
// - key does not contain '?', value is an empty array => just (key) // - key does not contain '?', value is an empty array => just (key)
// - key contains '?', value is a scalar or non-empty array => (key) with bind params (...value) // - key contains '?', value is a scalar or non-empty array => (key) with bind params (...value)
// - key is numeric, then value is treated as in array // - key is numeric, then value is treated as in array
export type Where = string export type Where = string
| (string | Text | (Value | (Value|Value[])[])[])[] | (string | Text | Value | Value[] | [ string, Value[]|Value[][] ])
| { [field: string]: Value | (Value | (Value|Value[])[])[] }; | { [field: string]: Value | Value[] | Value[][] };
export type SelectOptions = null | { export type SelectOptions = null | {
distinct_on?: string | string[], distinct_on?: string | string[],

View File

@ -1,11 +1,11 @@
// Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8 // Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8
// (c) Виталий Филиппов, 2019-2020 // (c) Виталий Филиппов, 2019-2021
// Версия 2020-11-17 // Версия 2021-09-14
// В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи, // В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи,
// благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ] // благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ]
// Здесь так нельзя, поэтому этот синтаксис мы заменяем на { 't1.a=t2.a': [], 't2.b': [ 1, 2, 3 ] } // Здесь так нельзя, поэтому этот синтаксис мы заменяем на { 't1.a=t2.a': [], 't2.b': [ 1, 2, 3 ] }
// Или на [ 't1.a=t2.a', [ 't2.b', 1, 2, 3 ] ] - можно писать и так, и так // Или на [ 't1.a=t2.a', [ 't2.b', [ 1, 2, 3 ] ] ] - можно писать и так, и так
const pg = require('pg'); const pg = require('pg');
const pg_escape = require('pg-escape'); const pg_escape = require('pg-escape');
@ -195,6 +195,47 @@ function tables_builder(tables)
return { sql, bind, moreWhere }; return { sql, bind, moreWhere };
} }
function where_in(key, values, result, bind, for_where)
{
if (values[0] instanceof Array)
{
// only for WHERE: [ '(a, b)', [ [ 1, 2 ], [ 3, 4 ], ... ] ]
if (!for_where)
{
throw new Error('IN syntax can only be used inside WHERE');
}
result.push(key + ' in (' + values.map(vi => '('+vi.map(() => '?').join(', ')+')') + ')');
values.forEach(vi => vi.forEach(v => bind.push(v)));
}
else if (!for_where)
{
if (values.length > 1)
{
throw new Error('IN syntax can only be used inside WHERE');
}
result.push(key+' = ?');
bind.push(values[0]);
}
else
{
// [ field, [ values ] ]
let non_null = values.filter(vi => vi != null);
let has_null = values.length > non_null.length;
if (non_null.length > 0)
{
result.push(
key+' in (' + non_null.map(() => '?').join(', ') + ')' +
(has_null ? ' or '+key+' is null' : '')
);
non_null.forEach(v => bind.push(v));
}
else if (has_null)
{
result.push(key+' is null');
}
}
}
// fields: one of: // fields: one of:
// - string: 'a=b AND c=d' // - string: 'a=b AND c=d'
// - array: [ 'a=b', [ 'a=? or b=?', 1, 2 ], [ 'a', [ 1, 2 ] ] ] // - array: [ 'a=b', [ 'a=? or b=?', 1, 2 ], [ 'a', [ 1, 2 ] ] ]
@ -203,86 +244,71 @@ function tables_builder(tables)
// - key does not contain '?', value is an empty array => just (key) // - key does not contain '?', value is an empty array => just (key)
// - key contains '?', value is a scalar or non-empty array => (key) with bind params (...value) // - key contains '?', value is a scalar or non-empty array => (key) with bind params (...value)
// - key is numeric, then value is treated as in array // - key is numeric, then value is treated as in array
function where_or_set(fields, for_condition) function where_or_set(fields, for_where)
{ {
if (typeof fields == 'string') if (typeof fields == 'string')
{ {
return { sql: fields, bind: [] }; return { sql: fields, bind: [] };
} }
const w = [], bind = []; const where = [], bind = [];
for (let k in fields) for (let k in fields)
{ {
let v = fields[k]; let v = fields[k];
v = v instanceof Array ? v : [ v ];
if (/^\d+$/.exec(k)) if (/^\d+$/.exec(k))
{ {
if (v instanceof Array) if (v.length == 0 || typeof v[0] !== 'string')
{ {
k = v[0]; // invalid value
v = v.slice(1); continue;
}
else if (v.length == 1)
{
// [ text ] or just text
where.push(v[0]);
}
else if (v[0].indexOf('?') >= 0)
{
// [ text, bind1, bind2, ... ]
// FIXME: check bind variable count
where.push(v[0]);
for (let i = 1; i < v.length; i++)
bind.push(v[i]);
} }
else else
{ {
w.push(v); // [ field, [ ...values ] ]
continue; if (v.length > 2)
{
throw new Error('Invalid condition: '+JSON.stringify(v));
}
v[1] = v[1] instanceof Array ? v[1] : [ v[1] ];
where_in(v[0], v[1], where, bind, for_where);
} }
} }
if (k.indexOf('?') >= 0 || v instanceof Array && v.length == 0)
{
if (!(v instanceof Array))
{
v = [ v ];
}
// FIXME: check bind variable count
w.push(k);
v.forEach(v => bind.push(v));
continue;
}
v = v instanceof Array ? v : [ v ];
if (v.length == 1 && v[0] == null)
{
w.push(for_condition ? k+' is null' : k+' = null');
}
else else
{ {
// a IN (...) or (a, b) IN ((...), ...) if (k.indexOf('?') >= 0)
if ((v.length > 1 || v[0] instanceof Array) && !for_condition)
{ {
throw new Error('IN syntax can only be used inside WHERE'); // { expr: [ bind ] }
} // FIXME: check bind variable count
if (v[0] instanceof Array) where.push(k);
{ for (let i = 0; i < v.length; i++)
// (a, b) in ((...), ...) bind.push(v[i]);
w.push(k + ' in (' + v.map(vi => '('+vi.map(() => '?').join(', ')+')') + ')');
v.forEach(vi => vi.forEach(v => bind.push(v)));
}
else if (!for_condition)
{
w.push(k+' = ?');
bind.push(v[0]);
} }
else else
{ {
let n = v.length; where_in(k, v, where, bind, for_where);
v = v.filter(vi => vi != null);
if (v.length > 0)
{
w.push(k+' in (' + v.map(() => '?').join(', ') + ')' + (n > v.length ? ' or '+k+' is null' : ''));
v.forEach(v => bind.push(v));
}
else if (n > 0)
{
w.push(k+' is null');
}
} }
} }
} }
if (!for_condition) if (!for_where)
{ {
// SET // SET
return { sql: w.join(', '), bind }; return { sql: where.join(', '), bind };
} }
// WHERE // WHERE
return { sql: w.length ? '('+w.join(') and (')+')' : '', bind }; return { sql: where.length ? '('+where.join(') and (')+')' : '', bind };
} }
function where_builder(where) function where_builder(where)
@ -443,6 +469,8 @@ async function insert(dbh, table, rows, options)
sql += ', '; sql += ', ';
if (row[k] == null) if (row[k] == null)
sql += 'default'; sql += 'default';
else if (row[k] instanceof Text)
sql += row[k].toString();
else else
sql += quote(row[k]); sql += quote(row[k]);
j++; j++;
@ -459,7 +487,7 @@ async function insert(dbh, table, rows, options)
? '('+options.upsert.join(', ')+')' ? '('+options.upsert.join(', ')+')'
: (typeof options.upsert == 'string' ? options.upsert : '(id)'))+ : (typeof options.upsert == 'string' ? options.upsert : '(id)'))+
' do update set '+ ' do update set '+
keys.map(k => `${k} = excluded.${k}`).join(', '); (options.upsert_fields || keys).map(k => `${k} = excluded.${k}`).join(', ');
} }
else if (options.ignore) else if (options.ignore)
{ {
@ -694,6 +722,7 @@ class Connection extends ConnectionBase
// Если уже кто-то активен - ждём его // Если уже кто-то активен - ждём его
await new Promise((resolve, reject) => this.transaction_queue.push(resolve)); await new Promise((resolve, reject) => this.transaction_queue.push(resolve));
} }
this.in_transaction = true;
const r = await this._query(sql, bind); const r = await this._query(sql, bind);
// Если есть ещё кто-то в очереди - пусть проходит // Если есть ещё кто-то в очереди - пусть проходит
this._next_txn(); this._next_txn();
@ -737,10 +766,6 @@ class Connection extends ConnectionBase
let start_time; let start_time;
try try
{ {
if (!this.in_transaction)
{
this.in_transaction = true;
}
if (this.config.log_queries) if (this.config.log_queries)
{ {
start_time = Date.now(); start_time = Date.now();
@ -754,25 +779,20 @@ class Connection extends ConnectionBase
console.log('> pid='+process.pid+' '+tm.toFixed(3)+' '+sql); console.log('> pid='+process.pid+' '+tm.toFixed(3)+' '+sql);
} }
} }
if (this.in_transaction === true)
{
this.in_transaction = false;
}
return r; return r;
} }
catch (e) catch (e)
{ {
// в postgresql надо откатывать всю транзакцию при любой ошибке // в postgresql надо откатывать всю транзакцию при любой ошибке
if (this.in_transaction === true)
{
this.in_transaction = false;
}
// не падать, если в процессе выполнения запроса отвалилось подключение // не падать, если в процессе выполнения запроса отвалилось подключение
if (this.dbh) if (this.dbh)
{ {
if (this.in_transaction) if (this.in_transaction)
{ {
await this.in_transaction.query('rollback'); if (this.in_transaction === true)
this._next_txn();
else
await this.in_transaction.query('rollback');
} }
else else
{ {
@ -794,20 +814,49 @@ class Transaction extends ConnectionBase
{ {
super(); super();
this.dbh = dbh; this.dbh = dbh;
this.nested = 0;
this.rolled_back = false;
}
async begin()
{
// Вложенная транзакция
// SAVEPOINT пока не поддерживаем - просто делаем вид, что началась вложенная транзакция
this.nested++;
return this;
} }
async commit() async commit()
{ {
await this.query('commit'); if (this.nested > 0)
{
this.nested--;
}
else
{
await this.query('commit');
}
} }
async rollback() async rollback()
{ {
await this.query('rollback'); if (!this.rolled_back)
{
await this.query('rollback');
this.rolled_back = true;
}
else if (this.nested > 0)
{
this.nested--;
}
} }
async query(sql, bind) async query(sql, bind)
{ {
if (this.rolled_back)
{
return null;
}
// Здесь уже ждать никого не надо, т.к. если мы сюда попали - то уже дождались своей очереди априори // Здесь уже ждать никого не надо, т.к. если мы сюда попали - то уже дождались своей очереди априори
const r = await this.dbh._query(sql, bind); const r = await this.dbh._query(sql, bind);
if (sql.length == 6 && sql.toLowerCase() == 'commit' || if (sql.length == 6 && sql.toLowerCase() == 'commit' ||