diff --git a/select-builder-pgsql.d.ts b/select-builder-pgsql.d.ts index c3a9685..3979600 100644 --- a/select-builder-pgsql.d.ts +++ b/select-builder-pgsql.d.ts @@ -25,15 +25,15 @@ export type Fields = string | string[] | { [field: string]: string }; export type Value = string | number | null | {}; // - 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 ] } // - 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 contains '?', value is a scalar or non-empty array => (key) with bind params (...value) // - key is numeric, then value is treated as in array export type Where = string - | (string | Text | (Value | (Value|Value[])[])[])[] - | { [field: string]: Value | (Value | (Value|Value[])[])[] }; + | (string | Text | Value | Value[] | [ string, Value[]|Value[][] ]) + | { [field: string]: Value | Value[] | Value[][] }; export type SelectOptions = null | { distinct_on?: string | string[], diff --git a/select-builder-pgsql.js b/select-builder-pgsql.js index bcbfa1c..584c2e2 100644 --- a/select-builder-pgsql.js +++ b/select-builder-pgsql.js @@ -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) + if (v.length == 0 || typeof v[0] !== 'string') { - k = v[0]; - v = v.slice(1); + // invalid value + 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 { - w.push(v); - continue; + // [ 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); } } - 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 { - // a IN (...) or (a, b) IN ((...), ...) - if ((v.length > 1 || v[0] instanceof Array) && !for_condition) + if (k.indexOf('?') >= 0) { - throw new Error('IN syntax can only be used inside WHERE'); - } - if (v[0] instanceof Array) - { - // (a, b) in ((...), ...) - 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]); + // { 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' ||