From b4dd25148f1bd901a559316ad08bc8b5dd6370c4 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Fri, 10 May 2019 01:27:32 +0300 Subject: [PATCH] New syntax and split_using support --- select-builder-pgsql.js | 351 +++++++++++++++++++++++++++++----------- 1 file changed, 255 insertions(+), 96 deletions(-) diff --git a/select-builder-pgsql.js b/select-builder-pgsql.js index 042c819..32d033c 100644 --- a/select-builder-pgsql.js +++ b/select-builder-pgsql.js @@ -1,9 +1,10 @@ // Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8 // (c) Виталий Филиппов, 2019 -// Версия 2019-05-08 +// Версия 2019-05-09 // В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи, // благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ] +// Здесь так нельзя, поэтому этот синтаксис мы заменяем на { 't1.a=t2.a': [], 't2.b': [ 1, 2, 3 ] } const pg = require('pg'); @@ -42,64 +43,16 @@ function select_builder(tables, fields, where, options) throw new Error('fields = '+fields+' is invalid'); } sql += ' FROM '; - let first = true; - let moreWhere = null; - tables = typeof tables == 'string' ? { t: tables } : tables; - for (const k in tables) + const t = tables_builder(tables); + sql += t.sql; + bind.push.apply(bind, t.bind); + where = where_builder(where); + sql += ' WHERE '+(where.sql || '1=1'); + bind.push.apply(bind, where.bind); + if (t.moreWhere) { - if (first) - { - if (typeof tables[k] != 'string') - { - // Бывает удобно указывать WHERE как условие "JOIN" первой таблицы - sql += tables[k][1] + ' ' + k; - moreWhere = tables[k][2]; - } - else - { - sql += tables[k] + ' ' + k; - } - first = false; - } - else if (typeof tables[k] == 'string') - { - sql += ' INNER JOIN '+tables[k]+' '+k+' ON 1=1'; - } - else - { - sql += ' ' + tables[k][0].toUpperCase() + ' JOIN '; - let t = tables[k][1]; - if (t instanceof Pg_Values) - { - sql += '(VALUES '; - let i = 0; - for (const row of t.rows) - { - sql += (i > 0 ? ', (' : '(') + t.keys.map(() => '$'+(++i)).join(', ')+')'; - bind.push.apply(bind, t.keys.map(k => row[k])); - } - sql += ') AS '+k+'('+t.keys.join(', ')+')'; - } - else - { - sql += t + ' ' + k; - } - const on = whereBuilder(tables[k][2]); - sql += ' ON ' + (on[0] || '1=1'); - bind.push.apply(bind, on[1]); - } - } - const w = whereBuilder(where); - sql += ' WHERE '+(w[0] || '1=1'); - bind.push.apply(bind, w[1]); - if (moreWhere) - { - moreWhere = whereBuilder(moreWhere); - if (moreWhere[0]) - { - sql += ' AND '+moreWhere[0]; - bind.push.apply(bind, moreWhere[1]); - } + sql += ' AND '+t.moreWhere.sql; + bind.push.apply(bind, t.moreWhere.bind); } options = options||{}; if (options['GROUP BY'] || options.group_by) @@ -122,51 +75,228 @@ function select_builder(tables, fields, where, options) { sql += ' LIMIT '+((options.OFFSET || options.offset) | 0); } - return [ sql, bind ]; + return { sql, bind }; } -function whereOrSetBuilder(fields, where) +function tables_builder(tables) +{ + let sql = '', bind = []; + let moreWhere = null; + let first = true; + if (typeof tables == 'string') + { + sql = tables; + return { sql, bind, moreWhere }; + } + for (const k in tables) + { + let jointype = 'INNER', table = tables[k], conds = null; + if (table instanceof Array) + { + [ jointype, table, conds ] = table; + } + if (!first) + { + sql += ' ' + jointype.toUpperCase() + ' JOIN '; + } + let more_on; + if (table instanceof Pg_Values) + { + sql += '(VALUES '; + let i = 0; + for (const row of table.rows) + { + sql += (i > 0 ? ', (' : '(') + table.keys.map(() => '?').join(', ')+')'; + bind.push.apply(bind, table.keys.map(k => row[k])); + i++; + } + sql += ') AS '+k+'('+table.keys.join(', ')+')'; + } + else if (typeof table == 'object') + { + // Nested join, `k` alias is ignored + let subjoin = tables_builder(table); + if (subjoin.moreWhere) + { + more_on = subjoin.moreWhere; + } + if (Object.keys(table).length > 1) + { + sql += "("+subjoin.sql+")"; + } + else + { + sql += subjoin.sql; + } + bind.push.apply(subjoin.bind); + } + else + { + sql += table + ' ' + k; + } + conds = where_builder(conds); + if (more_on) + { + if (!conds.sql) + conds = more_on; + else + { + conds.sql += ' AND ' + more_on.sql; + conds.bind.push.apply(conds.bind, more_on.bind); + } + } + if (!first) + { + sql += ' ON ' + (conds.sql || '1=1'); + bind.push.apply(bind, conds.bind); + } + else + { + // Бывает удобно указывать WHERE как условие "JOIN" первой таблицы + moreWhere = conds.sql ? conds : null; + first = false; + } + } + return { sql, bind, moreWhere }; +} + +// fields: one of: +// - string: 'a=b AND c=d' +// - array: [ 'a=b', [ 'a=? or b=?', 1, 2 ], [ 'a', [ 1, 2 ] ] ] +// - 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 +function where_or_set(fields, where) { if (typeof fields == 'string') - return [ fields, [] ]; + { + return { sql: fields, bind: [] }; + } const w = [], bind = []; - for (const k in fields) + for (let k in fields) { let v = fields[k]; - if (k.indexOf('?') >= 0) - { - if (!(v instanceof Array)) - v = [ v ]; - w.push(k); - bind.push.apply(bind, v); - } - else if (/^\d+$/.exec(k)) + if (/^\d+$/.exec(k)) { if (v instanceof Array) { - w.push(v[0]); - bind.push.apply(bind, v.slice(1)); + k = v[0]; + v = v.slice(1); } else { w.push(v); + continue; } } - else if (v != null || v instanceof Array && v.length) + if (k.indexOf('?') >= 0 || v instanceof Array && v.length == 0) { - v = v instanceof Array ? v : [ v ]; - w.push(v.length == 1 ? k + ' = ?' : k + ' in (' + v.map(() => '?').join(', ') + ')'); + if (!(v instanceof Array)) + { + v = [ v ]; + } + // FIXME: check bind variable count + w.push(k); bind.push.apply(bind, v); + continue; + } + v = v instanceof Array ? v : [ v ]; + if (v.length == 1 && v[0] == null) + { + w.push(where ? k+' is null' : k+' = null'); + } + else + { + if ((v.length > 1 || v[0] instanceof Array) && !where) + { + 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.map(vi => bind.push.apply(bind, vi)); + } + else + { + w.push(v.length == 1 + ? k + ' = ?' + : k + ' in (' + v.map(() => '?').join(', ') + ')'); + bind.push.apply(bind, v); + } } } if (!where) - return [ w.join(', '), bind ]; - return [ w.length ? '('+w.join(') and (')+')' : '', bind ]; + { + // SET + return { sql: w.join(', '), bind }; + } + // WHERE + return { sql: w.length ? '('+w.join(') and (')+')' : '', bind }; } -function whereBuilder(where) +function where_builder(where) { - return whereOrSetBuilder(where, true); + return where_or_set(where, true); +} + +/** + * Разбивает набор таблиц на основную обновляемую + набор дополнительных + * + * Идея в том, чтобы обрабатывать хотя бы 2 простые ситуации: + * UPDATE table1 INNER JOIN table2 ... + * UPDATE table1 LEFT JOIN table2 ... + */ +function split_using(tables) +{ + if (typeof tables == 'string') + { + return { what: { sql: tables, bind: [] }, using: null, moreWhere: null }; + } + let first = null; + let is_next_inner = true; + let i = 0; + for (let k in tables) + { + let t = tables[k]; + if (i == 0) + { + if (t instanceof Array && typeof(t[1]) != 'string') + { + throw new Error('Can only update/delete from real tables, not sub-select, sub-join or VALUES'); + } + first = k; + } + else if (i == 1) + { + is_next_inner = !(t instanceof Array) || t[0].toLowerCase() == 'inner'; + } + else + { + break; + } + i++; + } + let what, moreWhere; + if (is_next_inner) + { + what = tables_builder({ [first]: tables[first] }); + delete tables[first]; + moreWhere = what.moreWhere; + what.moreWhere = null; + } + else + { + what = tables_builder({ ["_"+first]: tables[first] }); + const cond = '_'+first+'.ctid='+(/^\d+$/.exec(first) ? tables[first] : first)+'.ctid'; + moreWhere = what.moreWhere + ? { sql: what.moreWhere.sql+' AND '+cond, bind: what.moreWhere.bind } + : { sql: cond, bind: [] }; + what.moreWhere = null; + } + return { what, using: Object.keys(tables).length > 0 ? tables : null, moreWhere }; } function _positional(sql) @@ -190,7 +320,7 @@ function _inline(sql, bind) // dbh = node-postgres.Client async function select(dbh, tables, fields, where, options, format) { - let [ sql, bind ] = select_builder(tables, fields, where, options); + let { sql, bind } = select_builder(tables, fields, where, options); //console.log(_inline(sql, bind)); let data = await dbh.query(_positional(sql), bind); if ((format & MS_LIST) || (format & MS_COL)) @@ -223,31 +353,59 @@ async function insert(dbh, table, rows, options) sql += (i > 0 ? ', (' : '(') + keys.map(() => '$'+(++i)).join(', ')+')'; bind.push.apply(bind, keys.map(k => row[k])); } - if (options.returning) + if (options && options.returning) { sql += ' returning '+options.returning; return (await dbh.query(sql, bind)).rows; } - else + return await dbh.query(sql, bind); +} + +async function _delete(dbh, table, where, options) +{ + where = where_builder(where); + const split = split_using(table); + if (split.using) { - return await dbh.query(sql, bind); + split.using = tables_builder(split.using); } + let sql = 'delete from '+split.what.sql+ + (split.using ? ' using '+split.using.sql : '')+ + ' where '+(where.sql || '1=1')+(split.moreWhere ? ' and '+split.moreWhere.sql : ''); + let bind = [ ...split.what.bind, ...where.bind, ...(split.moreWhere ? split.moreWhere.bind : []) ]; + if (options && options.returning) + { + sql += ' returning '+options.returning; + return (await dbh.query(_positional(sql), bind)).rows; + } + return await dbh.query(_positional(sql), bind); } -async function _delete(dbh, table, where) +async function update(dbh, table, set, where, options) { - const w = whereBuilder(where); - const sql = 'DELETE FROM '+table+' WHERE '+(w[0] || '1=1'); - return await dbh.execute(_positional(sql), w[1]); -} - -async function update(dbh, table, set, where) -{ - set = whereOrSetBuilder(set, false); - where = whereOrSetBuilder(where, true) - const sql = 'UPDATE '+table+' SET '+set[0]+' WHERE '+(where[0] || '1=1'); - const bind = [ ...set[1], ...where[1] ]; - return await dbh.execute(_positional(sql), bind); + set = where_or_set(set, false); + where = where_builder(where); + const split = split_using(table); + if (split.using) + { + split.using = tables_builder(split.using); + } + let sql = 'update '+split.what.sql+' set '+set.sql+ + (split.using ? ' from '+split.using.sql : '')+ + ' where '+(where.sql || '1=1')+(split.moreWhere ? ' and '+split.moreWhere.sql : ''); + let bind = [ + ...split.what.bind, + ...set.bind, + ...(split.using ? split.using.bind : []), + ...where.bind, + ...(split.moreWhere ? split.moreWhere.bind : []) + ]; + if (options && options.returning) + { + sql += ' returning '+options.returning; + return (await dbh.query(_positional(sql), bind)).rows; + } + return await dbh.query(_positional(sql), bind); } function values(rows) @@ -266,6 +424,7 @@ class Pg_Values module.exports = { select_builder, + where_builder, select, insert, delete: _delete,