New syntax and split_using support

master
Vitaliy Filippov 2019-05-10 01:27:32 +03:00
parent 43d8eb0d15
commit b4dd25148f
1 changed files with 255 additions and 96 deletions

View File

@ -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,