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