UNPKG

sql-soar

Version:

A SQL build and query tool for node.js.

651 lines (538 loc) 13.3 kB
/*! * sql-soar * authors: Ben Lue * license: MIT License * Copyright(c) 2015~2016 Gocharm Inc. */ var util = require('util'); var _debug = false, _NL = ' ', _noArgOP = ['IS NOT NULL', 'IS NULL']; /* turn on/off debug */ exports.setDebug = function setDebug(b) { _NL = (_debug = b) ? '\n' : ' '; }; exports.createTable = function(schema) { var debug = _debug || schema.debug, sql = util.format('CREATE TABLE %s\n(\n', schema.title); for (var i in schema.columns) { var c = schema.columns[i], s = util.format(' %s\t\t%s', c.title, c.type); if (c.options) { var opt = c.options; if (opt.notNull) s += ' NOT NULL'; if (opt.hasOwnProperty('default')) s += ' DEFAULT ' + opt.default; if (opt.autoInc) s += ' AUTO_INCREMENT'; if (opt.comment) s += " COMMENT '" + opt.comment + "'"; } sql += s + ',\n' } sql += ' primary key ('; var pk = ''; for (var i in schema.primary) { if (i > 0) pk += ', '; pk += schema.primary[i]; } sql += pk + ')\n)'; // adding table options var opts = ''; if (schema.options) { var opt = schema.options; for (var k in opt) { if (opts) opts += ' '; if (k === 'comment') opts += "COMMENT = '" + opt.comment + "'"; else opts += k + ' = ' + opt[k]; } } if (opt) sql += '\n' + opts + ';'; else sql += ';'; if (debug) console.log('SQL[Create table]-----\n' + sql); return sql; }; exports.alterTable = function(schema) { var sql = util.format('ALTER TABLE %s\n', schema.title), alterSpec, debug = _debug || schema.debug; if (schema.add) { if (schema.add.column) { var columns = schema.add.column; for (var i in columns) { var c = columns[i], s = util.format('ADD COLUMN %s\t%s', c.title, c.type); if (c.options) { var opt = c.options; if (opt.notNull) s += ' NOT NULL'; if (opt.hasOwnProperty('default')) s += ' DEFAULT ' + opt.default; if (opt.autoInc) s += ' AUTO_INCREMENT'; if (opt.comment) s += " COMMENT '" + opt.comment + "'"; } if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } if (schema.add.index) { var indexes = schema.add.index; for (var key in indexes) { var idx = indexes[key], isUnique = idx.unique ? ' UNIQUE' : ''; var idxSQL = ''; for (var i in idx.columns) { if (idxSQL) idxSQL += ', ' + idx.columns[i]; else idxSQL = idx.columns[i]; } var s = util.format('ADD%s INDEX %s (%s)', isUnique, key, idxSQL); if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } if (schema.add.foreignKey) { var fkeys = schema.add.foreignKey; for (var key in fkeys) { var fk = fkeys[key], delInt = 'restrict', updInt = 'restrict', reference = fk.reference; if (fk.integrity) { delInt = fk.integrity.delete || delInt; updInt = fk.integrity.update || updInt; } var idx = reference.indexOf('.'), refTable = reference.substring(0, idx), refCol = reference.substring(idx+1), s = util.format('ADD CONSTRAINT %s FOREIGN KEY (%s) references %s (%s) ON DELETE %s ON UPDATE %s', key, fk.key, refTable, refCol, delInt, updInt); if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } } if (schema.alter) { if (schema.alter.column) { var columns = schema.alter.column; for (var i in columns) { var c = columns[i], s; if (c.name) s = util.format('CHANGE COLUMN `%s` `%s` %s', c.title, c.name, c.type); else s = util.format('MODIFY COLUMN `%s` %s', c.title, c.type); if (s) { if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } } } if (schema.drop) { if (schema.drop.column) { var columns = schema.drop.column; for (var i in columns) { var s = 'DROP COLUMN ' + columns[i]; if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } if (schema.drop.index) { var indexes = schema.drop.index; for (var i in indexes) { var s = 'DROP INDEX ' + indexes[i]; if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } if (schema.drop.foreignKey) { var keys = schema.drop.foreignKey; for (var i in keys) { var s = 'DROP FOREIGN KEY ' + keys[i]; if (alterSpec) alterSpec += ',\n' + s; else alterSpec = s; } } } sql += alterSpec + ';'; if (debug) console.log('SQL[Alter table]-----\n' + sql); return sql; }; exports.toSQL = function(options, data, query, p) { var sql, op = options.op, expr = options.expr, fld = options.fields, debug = options.debug || _debug; switch (op) { case 'query': sql = genQuery(expr, query, p, fld, debug); break; case 'list': var range = options.range; sql = genList(expr, query, p, range, fld, debug); break; case 'listCount': sql = genListCount(); break; case 'insert': sql = genInsert(expr, data, p, debug); break; case 'update': sql = genUpdate(expr, data, query, p, debug); break; case 'delete': sql = genDelete(expr, query, p, debug); break; } return sql; }; function genQuery(expr, q, p, fld, debug) { var sql = composeQ(expr, q, p, fld, false, debug); if (sql.length > 0) sql += _NL + 'LIMIT 1;'; else sql += ';'; return sql; }; function genList(expr, q, p, range, fields, debug) { var isGetCount = range ? true : false, sql = composeQ( expr, q, p, fields, isGetCount, debug ); if (sql) { if (range) sql += _NL + 'LIMIT ' + range.getIndex() + ', ' + range.getPageSize() + ';'; else sql += ';'; } return sql; }; function genInsert(expr, data, p, debug) { if (!data) throw new Error('Missing insert data'); var tableSpec = rectifyTable(expr.table, true), fields = expr.columns; if (expr.table.join) fields = mainTableColumn(tableSpec, fields); var sql = 'INSERT INTO `' + tableSpec.table + '` (`', notFirst = false; fields.forEach( function(f) { if (data.hasOwnProperty(f)) { if (notFirst) sql += '`, `'; sql += f; notFirst = true; p.push( data[f] ); } }); sql += '`) VALUES (?'; for (var i = 1, len = p.length; i < len; i++) sql += ', ?'; sql += ');'; if (debug) { console.log('SQL-----\n' + sql); console.log('Arguments:\n' + p); } return sql; }; function genUpdate(expr, data, terms, p, debug) { if (!data) throw new Error('Missing update data'); var tableSpec = rectifyTable( expr.table, true ), fields = expr.columns; if (expr.table.join) fields = mainTableColumn(tableSpec, fields); var sql = util.format('UPDATE `%s`%sSET `', tableSpec.table, _NL), notFirst; fields.forEach( function(f) { if (data.hasOwnProperty(f)) { if (notFirst) sql += ', `'; sql += f + '`=?'; notFirst = true; p.push( data[f] ); } }); // where... var filter = expr.table.join ? mainTableFilter(tableSpec, expr.filters) : expr.filters; if (filter) { var s = matchFilter(filter, terms, p); if (s.length > 0) sql += _NL + 'WHERE ' + s; } sql += ';'; if (debug) { console.log('SQL-----\n' + sql); console.log('Arguments:\n' + p); } return sql; }; function genDelete(expr, terms, p, debug) { var tableSpec = rectifyTable( expr.table, true ), sql = util.format('DELETE FROM `%s`', tableSpec.table); // where... var filter = expr.table.join ? mainTableFilter(tableSpec, expr.filters) : expr.filters; if (filter !== null) { var s = matchFilter(filter, terms, p); if (s.length > 0) sql += _NL + 'WHERE ' + s; } sql += ';'; if (debug) { console.log('SQL-----\n' + sql); console.log('Arguments:\n' + p); } return sql; }; function genListCount() { return 'SELECT FOUND_ROWS() AS ct;'; } function composeQ(expr, q, p, fld, isGetCount, debug) { let table = expr.table, fields = expr.columns, sql = 'SELECT ', notFirst = false if (isGetCount) sql += 'SQL_CALC_FOUND_ROWS ' if (fld) // pick up only selected fields fields.forEach( function(f) { if (fld.indexOf(f) >= 0) { if (notFirst) sql += ', '; else notFirst = true; sql += f; } }); else fields.forEach( function(f) { if (notFirst) sql += ', '; else notFirst = true; sql += f; }); // from... sql += _NL + 'FROM ' + rectifyTable(table) // join... sql = genJoin(sql, table) // where... let filter = expr.filters; if (filter) { const s = matchFilter(filter, q, p) if (s.length > 0) sql += _NL + 'WHERE ' + s } // extra if (expr.extra) sql += _NL + expr.extra if (debug) { console.log('SQL-----\n' + sql) console.log('Arguments:\n' + p) } return sql }; /** * Generated the JOIN statement * @param {*} sql the SQL statment that has been generated thus far. * @param {*} table the table schema */ function genJoin(sql, table) { if (table.join) { table.join.forEach( function(jt) { sql += _NL; if (jt.type) sql += ' ' + jt.type + ' '; sql += 'JOIN ' + jt.table; if (jt.use) sql += ' USING(`' + jt.use + '`)'; else { var onWhat = jt.on || jt.onWhat; if (onWhat.indexOf('=') < 0) { // this is a short form as x.field, and we'll make it to x.field=y.field var idx = onWhat.indexOf('.'); // TODO: missing the syntax error handling if (idx > 0) { var jfield = onWhat.substring(idx), tbName = rectifyTable({name: jt.table}, true); onWhat += '=' + (tbName.alias || tbName.table) + jfield; } } sql += ' ON ' + onWhat; } }); } return sql; } function mainTableColumn(tableSpec, fields) { var nf = []; fields.forEach( function(f) { var idx = f.indexOf('.'); if (idx > 0) { // filter out columns from other tables var tbName = f.substring(0, idx); if (tbName === tableSpec.alias || tbName === tableSpec.table) nf.push( f.substring(idx+1) ); } }); return nf; } /** * remove all query terms not applicable to the main table */ function mainTableFilter(tableSpec, filter) { var op = filter.op; if (op === 'AND' || op === 'and' || op === 'OR' || op === 'or') { var nfilters = []; filter.filters.forEach(function(f) { var mf = mainTableFilter(tableSpec, f); if (mf) nfilters.push( mf ); }); if (nfilters.length) { if (nfilters.length === 1) return nfilters[0]; return {op: op.toUpperCase(), filters: nfilters}; } return null; } var fname = filter.name, idx = fname.indexOf('.'); if (idx > 0) { var alias = fname.substring(0, idx); if (alias !== tableSpec.alias && alias !== tableSpec.table) return null; fname = fname.substring(idx+1); } return {op: op, name: fname}; }; /** * q: input query parameter * p: the produced parameters for the prepared statement */ function matchFilter(filter, q, p) { // let's deal with the short cut first if (typeof filter === 'string') filter = {name: filter, op: '='}; else if (Array.isArray(filter)) filter = {op: 'AND', filters: filter} else if (Object.getOwnPropertyNames(filter).length === 1) { var key = Object.keys(filter)[0], fvalue = filter[key]; if (Array.isArray(fvalue)) { filter.filters = fvalue; filter.op = key; delete filter[key]; } else { var nf = { name: key, op: fvalue }; filter = nf; } } // console.log('filter is:\n' + JSON.stringify(filter, null, 4)); // now we're ready to roll... var op = filter.op, sql = ''; if (op === 'AND' || op === 'and' || op === 'OR' || op === 'or') { var hit = false; op = op.toUpperCase(); filter.filters.forEach(function(f) { var s = matchFilter(f, q, p); if (s) { sql = sql ? util.format('%s %s %s', sql, op, s) : s; hit = true; } }); if (hit) sql = '(' + sql + ')'; } else { var fname = filter.name, idx = fname.indexOf('.'); if (idx > 0) fname = fname.substring(idx+1); if (q.hasOwnProperty(fname)) { sql = filter.field || filter.name; if (op) { sql += ' ' + op; if (op === 'in' || op === 'IN') { var para = q[fname]; if (Object.prototype.toString.call(para) === '[object Array]' && para.length) { sql += ' ('; for (var i in para) { if (i > 0) sql += ', '; sql += '?'; p.push(para[i]); } sql += ')'; } else throw 'Parameters for the IN clause should be a non-empty array'; } else if (_noArgOP.indexOf(op) < 0) { if (!filter.noArg) { p.push( q[fname] ); sql += ' ?'; } } } else { p.push( q[fname] ); //sql += '=?'; } } } return sql; } function rectifyTable(table, fullSpec) { var tbName = table.name; var idx = tbName.indexOf('.'); if (idx > 0) tbName = tbName.substring(idx+1); if (fullSpec) { var spec = {}, idx0 = tbName.indexOf(' '); if (idx0 > 0) { var parts = tbName.split(' '); tbName = parts[0]; if (parts.length === 3) spec.alias = parts[2]; else spec.alias = parts[1]; } else spec.alias = tbName; spec.table = tbName; return spec; } return tbName; }