// Простенький "селект билдер" по мотивам MediaWiki-овского, успешно юзаю подобный в PHP уже лет 8 // (c) Виталий Филиппов, 2019 // Версия 2019-05-08 // В PHP, правда, прикольнее - там в массиве можно смешивать строковые и численные ключи, // благодаря чему можно писать $where = [ 't1.a=t2.a', 't2.b' => [ 1, 2, 3 ] ] const pg = require('pg'); // Сраный node-postgres конвертирует даты в Date и портит таймзону const DATATYPE_DATE = 1082; pg.types.setTypeParser(DATATYPE_DATE, function(val) { return val === null ? null : val; }); let pg_escape; const MS_HASH = 0; const MS_LIST = 1; const MS_ROW = 2; const MS_COL = 4; const MS_VALUE = 6; function select_builder(tables, fields, where, options) { let sql = 'SELECT ', bind = []; if (fields instanceof Array) { sql += fields.join(', '); } else if (typeof fields == 'string') { sql += fields; } else if (typeof fields == 'object') { sql += Object.keys(fields).map(k => fields[k]+' AS '+k).join(', '); } else { 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) { 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]); } } options = options||{}; if (options['GROUP BY'] || options.group_by) { let group = options['GROUP BY'] || options.group_by; group = group instanceof Array ? group : [ group ]; sql += ' GROUP BY '+group.join(', '); } if (options['ORDER BY'] || options.order_by) { let order = options['ORDER BY'] || options.order_by; order = order instanceof Array ? order : [ order ]; sql += ' ORDER BY '+order.join(', '); } if (options.LIMIT || options.limit) { sql += ' LIMIT '+((options.LIMIT || options.limit) | 0); } if (options.OFFSET || options.offset) { sql += ' LIMIT '+((options.OFFSET || options.offset) | 0); } return [ sql, bind ]; } function whereOrSetBuilder(fields, where) { if (typeof fields == 'string') return [ fields, [] ]; const w = [], bind = []; for (const 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 (v instanceof Array) { w.push(v[0]); bind.push.apply(bind, v.slice(1)); } else { w.push(v); } } else if (v != null || v instanceof Array && v.length) { v = v instanceof Array ? v : [ v ]; 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 ]; } function whereBuilder(where) { return whereOrSetBuilder(where, true); } function _positional(sql) { let i = 0; sql = sql.replace(/\?/g, () => '$'+(++i)); return sql; } function _inline(sql, bind) { if (!pg_escape) { pg_escape = require('pg-escape'); } let i = 0; sql = sql.replace(/\?/g, () => '\''+pg_escape.string(bind[i++])+'\''); return sql; } // dbh = node-postgres.Client async function select(dbh, tables, fields, where, options, format) { 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)) data = data.rows.map(r => Object.values(r)); else data = data.rows; if (format & MS_ROW) data = data[0]; if (data && (format & MS_COL)) data = data[0]; return data; } async function insert(dbh, table, rows, options) { if (!(rows instanceof Array)) { rows = [ rows ]; } if (!rows.length) { return null; } const keys = Object.keys(rows[0]); let sql = 'insert into '+table+' ('+keys.join(', ')+') values '; const bind = []; let i = 0; for (const row of rows) { sql += (i > 0 ? ', (' : '(') + keys.map(() => '$'+(++i)).join(', ')+')'; bind.push.apply(bind, keys.map(k => row[k])); } if (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) { 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); } function values(rows) { return new Pg_Values(Object.keys(rows[0]), rows); } class Pg_Values { constructor(keys, rows) { this.keys = keys; this.rows = rows; } } module.exports = { select_builder, select, insert, delete: _delete, update, values, MS_HASH, MS_LIST, MS_ROW, MS_COL, MS_VALUE, };