From e7b15ea67c9eb1bb1f9ca9ec568f0e8e5682582a Mon Sep 17 00:00:00 2001 From: Peter Rust Date: Mon, 25 Aug 2014 13:18:49 -0700 Subject: [PATCH 001/119] Major refactor to support dialect files --- index.html | 4 +- sql-bricks.js | 463 +++++++++++++++++++++++----------------------- tests/doctests.js | 4 +- tests/tests.js | 51 ++++- 4 files changed, 282 insertions(+), 240 deletions(-) diff --git a/index.html b/index.html index d43d12d..ac97a9e 100644 --- a/index.html +++ b/index.html @@ -448,8 +448,8 @@

select


Add the FOR UPDATE clause to lock all selected records from all tables in the select (or just the tables specified), along with an optional NO WAIT at the end:

-select('addr_id').from('person').forUpdate('addr_id').noWait();
-// SELECT addr_id FROM person FOR UPDATE addr_id NO WAIT
+select('addr_id').from('person').forUpdate().of('addr_id').noWait();
+// SELECT addr_id FROM person FOR UPDATE OF addr_id NO WAIT
 

diff --git a/sql-bricks.js b/sql-bricks.js index 5dc7940..da87637 100644 --- a/sql-bricks.js +++ b/sql-bricks.js @@ -22,6 +22,20 @@ function val(_val) { this.val = _val; } +// mechanism to easily define clauses for SQL statements +[Select, Insert, Update, Delete].forEach(function(stmt) { + stmt.defineClause = function(clause_id, template) { + if (!this.prototype.clauses) + this.prototype.clauses = []; + + var templ_fn = template; + if (typeof templ_fn != 'function') + templ_fn = function(opts) { return templ(template, this, opts); }; + this.prototype[clause_id + 'ToString'] = templ_fn; + this.prototype.clauses.push(clause_id); + }; +}); + // SELECT statement sql.select = inherits(Select, Statement); function Select() { @@ -32,44 +46,19 @@ function Select() { return this.select.apply(this, arguments); } -// COLUMNS clause -Select.prototype.select = function() { - return this._addListArgs(arguments, 'cols'); -}; +Select.prototype.select = addListMethod('_columns'); Select.prototype.distinct = function() { this._distinct = true; - return this._addListArgs(arguments, 'cols'); -}; -Select.prototype.columnsToString = function(opts) { - var cols = this.cols.length ? this.cols : ['*']; - var result = 'SELECT '; - if (this._distinct) - result += 'DISTINCT '; - return result + cols.map(curry(handleColOrTbl, opts)).join(', ') + ' '; + return this._addListArgs(arguments, '_columns'); }; - -// INTO clause -Select.prototype.into = Select.prototype.intoTable = function(tbl) { +Select.prototype.into = Select.prototype.intoTable = setAttrMethod('_into'); +Select.prototype.intoTemp = Select.prototype.intoTempTable = function(tbl) { + this._temp = true; this._into = tbl; return this; }; -Select.prototype.intoTemp = Select.prototype.intoTempTable = function(tbl) { - this._into_temp = true; - return this.into(tbl); -}; -Select.prototype.intoToString = function() { - if (!this._into) - return; - var result = 'INTO '; - if (this._into_temp) - result += 'TEMP '; - return result + this._into + ' '; -}; +Select.prototype.from = addListMethod('_from'); -// FROM clause (includes JOINs) -Select.prototype.from = function() { - return this._add(argsToArray(arguments), 'tbls'); -}; var join_methods = { 'join': 'INNER', 'innerJoin': 'INNER', 'leftJoin': 'LEFT', 'leftOuterJoin': 'LEFT', @@ -82,7 +71,6 @@ Object.keys(join_methods).forEach(function(method) { return this._addJoins(arguments, join_methods[method]); }; }); - Select.prototype.on = function(on) { var last_join = this.joins[this.joins.length - 1]; if (isExpr(on)) { @@ -96,93 +84,19 @@ Select.prototype.on = function(on) { return this; }; -Select.prototype.fromToString = function(opts) { - var result = ''; - if (this.tbls) - result += 'FROM ' + this.tbls.map(curry(handleTable, opts)).join(', ') + ' '; - if (this.joins) - result += _.invoke(this.joins, 'toString', opts).join(' ') + ' '; - return result; -}; - -// WHERE clause -Select.prototype.where = Select.prototype.and = function() { - return this._addExpression(arguments, '_where'); -}; -Select.prototype.whereToString = function(opts) { - if (this._where) - return 'WHERE ' + this._exprToString(opts); -}; - -// GROUP BY clause -Select.prototype.group = Select.prototype.groupBy = function(cols) { - return this._addListArgs(arguments, 'group_by'); -}; -Select.prototype.groupByToString = function(opts) { - if (this.group_by) - return 'GROUP BY ' + this.group_by.map(curry(handleColOrTbl, opts)).join(', ') + ' '; -}; - -// HAVING clause -Select.prototype.having = function() { - return this._addExpression(arguments, '_having'); -}; -Select.prototype.havingToString = function(opts) { - if (this._having) - return 'HAVING ' + this._exprToString(opts, this._having); -}; - -// ORDER BY clause -Select.prototype.order = Select.prototype.orderBy = function(cols) { - return this._addListArgs(arguments, 'order_by'); -}; -Select.prototype.orderByToString = function(opts) { - if (this.order_by) - return 'ORDER BY ' + this.order_by.map(curry(handleColOrTbl, opts)).join(', ') + ' '; -}; - -// LIMIT clause -Select.prototype.limit = function(count) { - this._limit = count; - return this; -}; -Select.prototype.limitToString = function(opts) { - if (this._limit != null) - return 'LIMIT ' + this._limit + ' '; -}; - -// OFFSET clause -Select.prototype.offset = function(count) { - this._offset = count; - return this; -}; -Select.prototype.offsetToString = function(opts) { - if (this._offset != null) - return 'OFFSET ' + this._offset + ' '; -} +Select.prototype.where = Select.prototype.and = addExpressionMethod('_where'); +Select.prototype.having = addExpressionMethod('_having'); +Select.prototype.groupBy = Select.prototype.group = addListMethod('_groupBy'); +Select.prototype.orderBy = Select.prototype.order = addListMethod('_orderBy'); +Select.prototype.of = addListMethod('_of'); -// FOR UPDATE clause -Select.prototype.forUpdate = Select.prototype.forUpdateOf = function forUpdate() { - this.for_update = true; - this._addListArgs(arguments, 'for_update_tbls'); - return this; -}; -Select.prototype.noWait = function noWait() { - this.no_wait = true; - return this; -}; -Select.prototype.forUpdateToString = function(opts) { - if (!this.for_update) - return; - var result = 'FOR UPDATE '; - if (this.for_update_tbls) - result += this.for_update_tbls.map(curry(handleTable, opts)).join(', ') + ' '; - if (this.no_wait) - result += 'NO WAIT '; - return result; -}; +// TODO: shouldn't LIMIT/OFFSET use handleValue()? Otherwise isn't it vulnerable to SQL Injection? +Select.prototype.limit = setAttrMethod('_limit'); +Select.prototype.offset = setAttrMethod('_offset'); +Select.prototype.forUpdate = setBoolMethod('_forUpdate'); +Select.prototype.noWait = setBoolMethod('_noWait'); -// compound expressions join queries together +// TODO: Don't we need to keep track of the order of UNION, INTERSECT, etc, clauses? var compounds = { 'union': 'UNION', 'unionAll': 'UNION ALL', 'intersect': 'INTERSECT', 'intersectAll': 'INTERSECT ALL', @@ -213,32 +127,44 @@ Select.prototype.as = function(alias) { return this; }; -sql.select.clauses = ['columns', 'into', 'from', 'where', 'groupBy', 'having', 'orderBy', 'limit', 'offset', 'forUpdate']; Select.prototype._toString = function _toString(opts) { - var result = ''; + if (!this._columns.length) + this._columns = ['*']; + return Select.super_.prototype._toString.apply(this, arguments); +}; - // build main SELECT statement - sql.select.clauses.forEach(function(clause) { - var rlt = this[clause + 'ToString'](opts); - if (rlt) - result += rlt; - }.bind(this)); +Select.defineClause('select', 'SELECT {{#if _distinct}}DISTINCT {{/if}}{{#if _columns}}{{columns _columns}}{{/if}}'); +Select.defineClause('into', '{{#if _into}}INTO {{#if _temp}}TEMP {{/if}}{{table _into}}{{/if}}'); +Select.defineClause('from', function(opts) { + if (!this._from) + return; + var result = 'FROM ' + handleTables(this._from); + if (this.joins) + result += ' ' + _.invoke(this.joins, 'toString', opts).join(' '); + return result; +}); +Select.defineClause('where', '{{#if _where}}WHERE {{expression _where}}{{/if}}'); +Select.defineClause('groupBy', '{{#if _groupBy}}GROUP BY {{columns _groupBy}}{{/if}}'); +Select.defineClause('having', '{{#if _having}}HAVING {{expression _having}}{{/if}}'); - // handle any compound statements - _.forEach(compounds, function(value, key) { - var arr = this['_' + key]; +_.forEach(compounds, function(sql_keyword, clause_id) { + Select.defineClause(clause_id, function(opts) { + var arr = this['_' + clause_id]; if (arr) { - result += value + ' '; - result += arr.map(function(stmt) { - return stmt._toString(opts); - }).join(' ' + value + ' '); + return arr.map(function(stmt) { + return sql_keyword + ' ' + stmt._toString(opts); + }).join(' '); } - }.bind(this)); + }); +}); - return result.trim(); -}; +Select.defineClause('orderBy', '{{#if _orderBy}}ORDER BY {{columns _orderBy}}{{/if}}'); +Select.defineClause('limit', '{{#ifNotNull _limit}}LIMIT {{_limit}}{{/ifNotNull}}'); +Select.defineClause('offset', '{{#ifNotNull _offset}}OFFSET {{_offset}}{{/ifNotNull}}'); +Select.defineClause('forUpdate', '{{#if _forUpdate}}FOR UPDATE{{#if _of}} OF {{columns _of}}{{/if}}{{#if _noWait}} NO WAIT{{/if}}{{/if}}'); +// INSERT statement sql.insert = sql.insertInto = inherits(Insert, Statement); function Insert(tbl, values) { if (!(this instanceof Insert)) { @@ -254,7 +180,7 @@ function Insert(tbl, values) { Insert.prototype.into = function into(tbl, values) { if (tbl) - this.tbls = [tbl]; + this._table = tbl; if (values) { if (isPlainObject(values) || (_.isArray(values) && isPlainObject(values[0]))) { @@ -308,47 +234,36 @@ Insert.prototype.select = function select() { this._select.prev_stmt = this; return this._select; }; -Insert.prototype.returning = function returning() { - this._addListArgs(arguments, '_returning'); - return this; -}; -Insert.prototype._toString = function _toString(opts) { - var keys = _.map(_.keys(this._values[0]), function(col) { - return handleColOrTbl(opts, col); - }).join(', '); + +Insert.prototype.returning = addListMethod('_returning'); + +Insert.defineClause('insert', 'INSERT'); +Insert.defineClause('or', '{{#if _or}}OR {{_or}}{{/if}}'); +Insert.defineClause('into', '{{#if _table}}INTO {{table _table}}{{/if}}'); +Insert.defineClause('columns', function(opts) { + return '(' + handleColumns(_.keys(this._values[0]), opts) + ')'; +}); +Insert.defineClause('values', function(opts) { var values = _.map(this._values, function(values) { - return '(' + _.map(_.values(values), function(val) { - return handleValue(val, opts); - }).join(', ') + ')'; + return '(' + handleValues(_.values(values), opts).join(', ') + ')'; }).join(', '); - var sql = 'INSERT '; - if (this._or) - sql += 'OR ' + this._or + ' '; - sql += 'INTO ' + this.tbls.map(curry(handleTable, opts)).join(', ') + ' (' + keys + ') '; - if (this._select) - sql += this._select._toString(opts) + ' '; + return this._select._toString(opts); else - sql += 'VALUES ' + values + ' '; - - if (this._returning) { - sql += 'RETURNING ' + _.map(this._returning, function(col) { - return handleColOrTbl(opts, col); - }).join(', '); - } - - return sql.trim(); -}; + return 'VALUES ' + values; +}); +Insert.defineClause('returning', '{{#if _returning}}RETURNING {{columns _returning}}{{/if}}'); +// UPDATE statement sql.update = inherits(Update, Statement); function Update(tbl, values) { if (!(this instanceof Update)) return new Update(tbl, argsToObject(_.toArray(arguments).slice(1))); Update.super_.call(this, 'update'); - this.tbls = [tbl]; + this._table = tbl; if (values) this.values(values); return this; @@ -359,30 +274,20 @@ Update.prototype.set = Update.prototype.values = function set() { }; Update.prototype.where = Update.prototype.and = Select.prototype.where; - Update.prototype.returning = Insert.prototype.returning; -Update.prototype._toString = function _toString(opts) { - var sql = 'UPDATE '; - if (this._or) - sql += 'OR ' + this._or + ' '; - sql += handleTable(opts, this.tbls[0]) + ' SET '; - sql += _.map(this._values, function(value, key) { - return handleColOrTbl(opts, key) + ' = ' + handleValue(value, opts); - }).join(', ') + ' '; - - if (this._where) - sql += 'WHERE ' + this._exprToString(opts); - - if (this._returning) { - sql += 'RETURNING ' + _.map(this._returning, function(col) { - return handleColOrTbl(opts, col); - }).join(', '); - } - return sql.trim(); -}; +Update.defineClause('update', 'UPDATE'); +Update.defineClause('or', '{{#if _or}}OR {{_or}}{{/if}}'); +Update.defineClause('table', '{{table _table}}'); +Update.defineClause('set', function(opts) { + return 'SET ' + _.map(this._values, function(value, key) { + return handleColumn(key, opts) + ' = ' + handleValue(value, opts); + }).join(', '); +}); +Update.defineClause('where', '{{#if _where}}WHERE {{expression _where}}{{/if}}'); +Update.defineClause('returning', '{{#if _returning}}RETURNING {{columns _returning}}{{/if}}'); -// Insert & Update OR clauses +// Insert & Update OR clauses (SQLite dialect) var or_methods = { 'orReplace': 'REPLACE', 'orRollback': 'ROLLBACK', 'orAbort': 'ABORT', 'orFail': 'FAIL' @@ -393,7 +298,8 @@ Object.keys(or_methods).forEach(function(method) { }; }); -// Delete + +// DELETE statement sql.delete = sql.deleteFrom = inherits(Delete, Statement); function Delete(tbl) { if (!(this instanceof Delete)) @@ -401,29 +307,24 @@ function Delete(tbl) { Delete.super_.call(this, 'delete'); if (tbl) - this.tbls = [tbl]; + this._from = tbl; return this; } -Delete.prototype.from = Select.prototype.from; -Delete.prototype.using = function using() { - return this._add(argsToArray(arguments), '_using'); -}; +Delete.prototype.from = setAttrMethod('_from'); +Delete.prototype.using = addListMethod('_using'); Delete.prototype.where = Delete.prototype.and = Select.prototype.where; -Delete.prototype._toString = function _toString(opts) { - var sql = 'DELETE FROM ' + handleTable(opts, this.tbls[0]) + ' '; - if (this._using) - sql += 'USING ' + this._using.map(curry(handleTable, opts)).join(', ') + ' '; - if (this._where) - sql += 'WHERE ' + this._exprToString(opts); - return sql.trim(); -}; +Delete.defineClause('delete', 'DELETE FROM {{table _from}}'); +Delete.defineClause('using', '{{#if _using}}USING {{tables _using}}{{/if}}'); +Delete.defineClause('where', '{{#if _where}}WHERE {{expression _where}}{{/if}}'); +// base statement sql.Statement = Statement; function Statement(type) { this.type = type; }; +// TODO: this seems to not handle... a *lot* of properties Statement.prototype.clone = function clone() { var ctor = _.find([Select, Insert, Update, Delete], function(ctor) { return this instanceof ctor; @@ -467,14 +368,14 @@ Statement.prototype.toString = function toString() { return this._toString({}).trim(); }; - -Statement.prototype._exprToString = function _exprToString(opts, expr) { - if (!expr) - expr = this._where; - expr.parens = false; - if (expr.expressions && expr.expressions.length == 1) - expr.expressions[0].parens = false; - return expr.toString(opts) + ' '; +Statement.prototype._toString = function(opts) { + var result = ''; + this.clauses.forEach(function(clause) { + var rlt = this[clause + 'ToString'](opts); + if (rlt) + result += rlt + ' '; + }.bind(this)); + return result.trim(); }; Statement.prototype._add = function _add(arr, name) { @@ -522,7 +423,7 @@ Statement.prototype._addJoins = function _addJoins(args, type) { } _.forEach(tbls, function(tbl) { - var left_tbl = this.last_join || (this.tbls && this.tbls[this.tbls.length - 1]); + var left_tbl = this.last_join || (this._from && this._from[this._from.length - 1]); this.joins.push(new Join(tbl, left_tbl, on, type)); }.bind(this)); @@ -530,6 +431,32 @@ Statement.prototype._addJoins = function _addJoins(args, type) { return this; }; +function setAttrMethod(attr) { + return function(tbl) { + this[attr] = tbl; + return this; + }; +} + +function setBoolMethod(attr) { + return function() { + this[attr] = true; + return this; + }; +} + +function addListMethod(attr) { + return function() { + return this._addListArgs(arguments, attr); + }; +} + +function addExpressionMethod(attr) { + return function() { + return this._addExpression(arguments, attr); + }; +} + function Join(tbl, left_tbl, on, type) { this.tbl = tbl; @@ -542,7 +469,7 @@ Join.prototype.autoGenerateOn = function autoGenerateOn(tbl, left_tbl) { return sql._joinCriteria(getTable(left_tbl), getAlias(left_tbl), getTable(tbl), getAlias(tbl)); }; Join.prototype.toString = function toString(opts) { - var on = this.on, tbl = handleTable(opts, this.tbl), left_tbl = handleTable(opts, this.left_tbl); + var on = this.on, tbl = handleTable(this.tbl, opts), left_tbl = handleTable(this.left_tbl, opts); if (!on || _.isEmpty(on)) { if (sql._joinCriteria) on = this.autoGenerateOn(tbl, left_tbl); @@ -555,17 +482,12 @@ Join.prototype.toString = function toString(opts) { } else { on = _.map(_.keys(on), function(key) { - return handleColOrTbl(opts, key) + ' = ' + handleColOrTbl(opts, on[key]); + return handleColumn(key, opts) + ' = ' + handleColumn(on[key], opts); }).join(' AND ') } return this.type + ' JOIN ' + tbl + ' ON ' + on; }; -// simple single-arg curry -function curry(fn, arg) { - return fn.bind(null, arg); -} - // handle an array, a comma-delimited str or separate args function argsToArray(args) { if (_.isArray(args[0])) @@ -605,8 +527,8 @@ function argsToExpressions(args) { } } -// SQL Expression language +// SQL Expression language sql.and = function and() { return new Group('AND', argsToArray(arguments)); }; sql.or = function or() { return new Group('OR', argsToArray(arguments)); }; @@ -684,7 +606,7 @@ Binary.prototype.clone = function clone() { return new Binary(this.op, this.col, this.val); }; Binary.prototype.toString = function toString(opts) { - var sql = handleColOrTbl(opts, this.col); + var sql = handleColumn(this.col, opts); return sql + ' ' + this.op + ' ' + this.quantifier + handleValue(this.val, opts); } @@ -698,7 +620,7 @@ Like.prototype.clone = function clone() { return new Like(this.col, this.val, this.escape_char); }; Like.prototype.toString = function toString(opts) { - var sql = handleColOrTbl(opts, this.col) + ' LIKE ' + handleValue(this.val, opts); + var sql = handleColumn(this.col, opts) + ' LIKE ' + handleValue(this.val, opts); if (this.escape_char) sql += " ESCAPE '" + this.escape_char + "'"; return sql; @@ -714,7 +636,7 @@ Between.prototype.clone = function clone() { return new Between(this.col, this.val1, this.val2); }; Between.prototype.toString = function(opts) { - return handleColOrTbl(opts, this.col) + ' BETWEEN ' + handleValue(this.val1, opts) + ' AND ' + handleValue(this.val2, opts); + return handleColumn(this.col, opts) + ' BETWEEN ' + handleValue(this.val1, opts) + ' AND ' + handleValue(this.val2, opts); }; sql.isNull = function isNull(col) { return new Unary('IS NULL', col); }; @@ -728,7 +650,7 @@ Unary.prototype.clone = function clone() { return new Unary(this.op, this.col); }; Unary.prototype.toString = function toString(opts) { - return handleColOrTbl(opts, this.col) + ' ' + this.op; + return handleColumn(this.col, opts) + ' ' + this.op; }; sql['in'] = function(col, list) { @@ -746,16 +668,13 @@ In.prototype.clone = function clone() { return new In(this.col, this.list.slice()); }; In.prototype.toString = function toString(opts) { - var col_sql = handleColOrTbl(opts, this.col); + var col_sql = handleColumn(this.col, opts); var sql; - if (_.isArray(this.list)) { - sql = _.map(this.list, function(val) { - return handleValue(val, opts); - }).join(', '); - } - else if (this.list instanceof Statement) { + if (_.isArray(this.list)) + sql = handleValues(this.list, opts).join(', '); + else if (this.list instanceof Statement) sql = this.list._toString(opts); - } + return col_sql + ' IN (' + sql + ')'; }; @@ -806,6 +725,18 @@ function objToEquals(obj) { return expressions; } +function handleExpression(expr, opts) { + expr.parens = false; + if (expr.expressions && expr.expressions.length == 1) + expr.expressions[0].parens = false; + return expr.toString(opts); +} + +function handleValues(vals, opts) { + return vals.map(function(val) { + return handleValue(val, opts); + }); +} function handleValue(val, opts) { if (val instanceof Statement) return '(' + val._toString(opts) + ')'; @@ -839,14 +770,20 @@ sql.conversions = { 'Array': function(arr) { return '{' + arr.map(sql.convert).join(', ') + '}'; } }; -function handleTable(opts, expr) { - return handleColOrTbl(opts, expandAlias(expr)); +function handleTables(tables, opts) { + return tables.map(function(tbl) { return handleTable(tbl, opts); }).join(', '); +} +function handleTable(table, opts) { + return handleColumn(expandAlias(table), opts); } +function handleColumns(cols, opts) { + return cols.map(function(col) { return handleColumn(col, opts); }).join(', '); +} // handles prefixes before a '.' and suffixes after a ' ' // for example: 'tbl.order AS tbl_order' -> 'tbl."order" AS tbl_order' var unquoted_regex = /^[\w\.]+(( AS)? \w+)?$/i; -function handleColOrTbl(opts, expr) { +function handleColumn(expr, opts) { if (expr instanceof Statement) { var result = '(' + expr._toString(opts) + ')'; if (expr._alias) { @@ -913,6 +850,70 @@ sql.joinCriteria = function joinCriteria(fn) { }; +// uber-simple mini-templating language to make it easy to define clauses +// handlebars-like syntax, supports helpers and nested blocks +// does not support context changes, the dot operator on properties or HTML escaping +function templ(str, ctx, opts) { + var result = ''; + var lastIndex = 0; + + var tmpl_re = /\{\{([#\/])?(\w+) ?(\w+)?\}\}/g; + var m; + while (m = tmpl_re.exec(str)) { + var is_block = m[1]; + var is_start = m[1] == '#'; + if (m[3]) { + var fn_name = m[2], attr = m[3]; + var helper = templ.helpers[fn_name]; + } + else { + var attr = m[2]; + } + var val = ctx[attr]; + result += str.slice(lastIndex, m.index); + + if (is_block) { + if (is_start) { + var end_re = new RegExp("\\{\\{([#/])" + fn_name + ' ?(\\w+)?\\}\\}', 'g'); + end_re.lastIndex = tmpl_re.lastIndex; + // incr & decr level 'til we find the end block that matches this start block + var level = 1; + while (level) { + var end_m = end_re.exec(str); + if (!end_m) + throw new Error('End not found for block ' + fn_name); + if (end_m[1] == '#') + level++; + else + level--; + } + var contents = str.slice(tmpl_re.lastIndex, end_m.index); + result += helper.call(ctx, val, opts, contents, ctx); + lastIndex = tmpl_re.lastIndex = end_re.lastIndex; + } + } + else { + if (fn_name) + result += helper.call(ctx, val, opts); + else + result += val; + lastIndex = tmpl_re.lastIndex; + } + } + result += str.slice(lastIndex); + return result; +} +sql.templ = templ; + +templ.helpers = { + 'if': function(val, opts, contents, ctx) { return val ? templ(contents, ctx, opts) : ''; }, + 'ifNotNull': function(val, opts, contents, ctx) { return val != null ? templ(contents, ctx, opts) : ''; }, + 'columns': handleColumns, + 'table': handleTable, + 'tables': handleTables, + 'expression': handleExpression +}; + // provided for browser support, based on https://gist.github.com/prust/5936064 function inherits(ctor, superCtor) { if (Object.create) { diff --git a/tests/doctests.js b/tests/doctests.js index 5b5d067..a4e6d35 100644 --- a/tests/doctests.js +++ b/tests/doctests.js @@ -107,8 +107,8 @@ it(".select().from('person').where({'last_name': 'Rubble'});", function() { check(select().from('person').where({'last_name': 'Flintstone'}).union() .select().from('person').where({'last_name': 'Rubble'}), "SELECT * FROM person WHERE last_name = 'Flintstone' UNION SELECT * FROM person WHERE last_name = 'Rubble'"); }); -it("select('addr_id').from('person').forUpdate('addr_id').noWait();", function() { -check(select('addr_id').from('person').forUpdate('addr_id').noWait(), "SELECT addr_id FROM person FOR UPDATE addr_id NO WAIT"); +it("select('addr_id').from('person').forUpdate().of('addr_id').noWait();", function() { +check(select('addr_id').from('person').forUpdate().of('addr_id').noWait(), "SELECT addr_id FROM person FOR UPDATE OF addr_id NO WAIT"); }); it("insert('person', {'first_name': 'Fred', 'last_name': 'Flintstone'});", function() { diff --git a/tests/tests.js b/tests/tests.js index 66eafcf..2ec1614 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -18,6 +18,15 @@ else { assert.deepEqual = function(actual, expected) { if (!_.isEqual(actual, expected)) throw new Error(JSON.stringify(actual) + ' == ' + JSON.stringify(expected)); }; + assert.throws = function(fn) { + try { + fn(); + } + catch(ex) { + return true; + } + throw new Error('The function passed to assert.throws() did not throw'); + } } var select = sql.select, insertInto = sql.insertInto, insert = sql.insert, @@ -39,6 +48,34 @@ sql.joinCriteria(function(left_tbl, left_alias, right_tbl, right_alias) { }); describe('SQL Bricks', function() { + describe('mini-templating lang', function() { + it('should find content of if', function() { + var result = sql.templ('{{#if test}}Hi there!{{/if}}', {'test': true}); + assert.equal(result, 'Hi there!'); + }); + it('should display content on both sides of it', function() { + var result = sql.templ('before{{#if test}}inside{{/if}}after', {'test': false}); + assert.equal(result, 'beforeafter'); + }); + it('should handle multiple values', function() { + var result = sql.templ('before{{val}}between{{val2}}after', {'val': 'value 1', 'val2': 'value 2'}); + assert.equal(result, 'beforevalue 1betweenvalue 2after'); + }); + it('should handle multiple if chunks', function() { + var result = sql.templ('before{{#if test}}first{{/if}}between{{#if test}}second{{/if}}after', {'test': true}); + assert.equal(result, 'beforefirstbetweensecondafter'); + }); + it('should throw on mismatched if', function() { + assert.throws(function() { + sql.templ('{{#if test}}Hi there!', {'test': true}); + }); + }); + it('should handle nested if', function() { + var result = sql.templ('{{#if oneThing}} and {{#if anotherThing}}Test{{/if}}{{/if}}', {'oneThing': true, 'anotherThing': true}); + assert.equal(result, ' and Test'); + }); + }); + describe('parameterized sql', function() { it('should generate for insert statements', function() { var values = {'first_name': 'Fred', 'last_name': 'Flintstone'}; @@ -322,12 +359,16 @@ describe('SQL Bricks', function() { 'SELECT DISTINCT one, "order", two, "desc" FROM "user"'); }); it('should support FOR UPDATE', function() { - check(select().from('user').forUpdate('user'), - 'SELECT * FROM "user" FOR UPDATE "user"'); + check(select().from('user').forUpdate(), + 'SELECT * FROM "user" FOR UPDATE'); + }); + it('should support FOR UPDATE w/ col list', function() { + check(select().from('user').forUpdate().of('user'), + 'SELECT * FROM "user" FOR UPDATE OF "user"'); }); - it('should support FOR UPDATE ... NO WAIT', function() { - check(select().from('user').forUpdateOf('user').noWait(), - 'SELECT * FROM "user" FOR UPDATE "user" NO WAIT'); + it('should support FOR UPDATE OF ... NO WAIT', function() { + check(select().from('user').forUpdate().of('user').noWait(), + 'SELECT * FROM "user" FOR UPDATE OF "user" NO WAIT'); }); }); From 0e422a6654aa3380787a23529853d277883873a5 Mon Sep 17 00:00:00 2001 From: Peter Rust Date: Mon, 25 Aug 2014 13:26:27 -0700 Subject: [PATCH 002/119] Ver bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aaae949..70c6482 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sql-bricks", - "version": "0.12.0", + "version": "0.13.0", "author": "Peter Rust ", "description": "Transparent, Schemaless SQL Generation", "homepage": "http://csnw.github.io/sql-bricks", From 432aa5742d5ffd2832556a6aa40c96650e5c4742 Mon Sep 17 00:00:00 2001 From: Anton Lyxell Date: Thu, 28 Aug 2014 23:43:03 +0200 Subject: [PATCH 003/119] Adding returning clause to delete statement (postgresql) --- sql-bricks.js | 3 +++ tests/tests.js | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/sql-bricks.js b/sql-bricks.js index da87637..79fe217 100644 --- a/sql-bricks.js +++ b/sql-bricks.js @@ -313,9 +313,12 @@ function Delete(tbl) { Delete.prototype.from = setAttrMethod('_from'); Delete.prototype.using = addListMethod('_using'); Delete.prototype.where = Delete.prototype.and = Select.prototype.where; +Delete.prototype.returning = Insert.prototype.returning; + Delete.defineClause('delete', 'DELETE FROM {{table _from}}'); Delete.defineClause('using', '{{#if _using}}USING {{tables _using}}{{/if}}'); Delete.defineClause('where', '{{#if _where}}WHERE {{expression _where}}{{/if}}'); +Delete.defineClause('returning', '{{#if _returning}}RETURNING {{columns _returning}}{{/if}}'); // base statement diff --git a/tests/tests.js b/tests/tests.js index 2ec1614..0189693 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -846,8 +846,12 @@ describe('SQL Bricks', function() { check(del('user').using('addr').where('user.addr_fk', sql('addr.pk')), "DELETE FROM \"user\" USING address addr WHERE \"user\".addr_fk = addr.pk"); }); - }); + it('should handle RETURNING (postgres dialect)', function() { + check(del('user').where({'lname': 'Flintstone'}).returning('*'), + "DELETE FROM \"user\" WHERE lname = 'Flintstone' WHERE lname = 'Flintstone' RETURNING *"); }); + }); +}); function check(stmt, expected) { assert.equal(stmt.toString(), expected); From 69f8b0b4d02b26e81b7830217c842aa1eb7c4723 Mon Sep 17 00:00:00 2001 From: Anton Lyxell Date: Thu, 28 Aug 2014 23:47:34 +0200 Subject: [PATCH 004/119] Typo fix in test --- tests/tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.js b/tests/tests.js index 0189693..d5edac5 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -848,7 +848,7 @@ describe('SQL Bricks', function() { }); it('should handle RETURNING (postgres dialect)', function() { check(del('user').where({'lname': 'Flintstone'}).returning('*'), - "DELETE FROM \"user\" WHERE lname = 'Flintstone' WHERE lname = 'Flintstone' RETURNING *"); + "DELETE FROM \"user\" WHERE lname = 'Flintstone' RETURNING *"); }); }); }); From 6517a05d3aedb5036ed09235a7f72276ff619fef Mon Sep 17 00:00:00 2001 From: Peter Rust Date: Fri, 29 Aug 2014 06:24:32 -0700 Subject: [PATCH 005/119] updated docs: pg-bricks & ~800 lines --- index.html | 5 ++++- readme.md | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index ac97a9e..2a7077e 100644 --- a/index.html +++ b/index.html @@ -236,7 +236,10 @@

SQL Bricks.js

In addition, SQL Bricks contains a few conveniences to aid in re-use and to make SQL generation a little less of a chore: automatic quoting of columns that collide with keywords (order, desc, etc) & columns that contain capital letters, automatic alias expansion, user-supplied join criteria functions.

SQL Bricks differs from similar libraries in that it does not require a schema and it is designed to be transparent, matching SQL so faithfully that developers with SQL experience will immediately know the API.

SQL Bricks supports the four CRUD statements (SELECT, INSERT, UPDATE, DELETE) and all of their clauses as defined by SQL-92 as well as some additional clauses supported by Postgres and SQLite. Adding support for other SQL statements (CREATE, ALTER TABLE, etc) would clutter the library without providing much real benefit.

-

The source is on GitHub and over 175 tests are available for your perusal.

+

The source is on GitHub and over 200 tests are available for your perusal.

+ +

Related Libraries

+

pg-bricks adds postgres connections, transactions, query execution and data accessors on top of SQLBricks.

Use

diff --git a/readme.md b/readme.md index 8101c91..19edf7f 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ SQL Bricks.js is a transparent, schemaless library for building and composing SQL statements. - Supports all [SQL-92](http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt) clauses for select/insert/update/delete (plus some postgres & sqlite additions) -- Over [175 tests](http://csnw.github.io/sql-bricks/browser-tests.html) +- Over [200 tests](http://csnw.github.io/sql-bricks/browser-tests.html) - Easy-to-use, comprehensive [docs](http://csnw.github.io/sql-bricks) - Single straightforward [source file](sql-bricks.js) (less than 1,000 lines), easy to understand & debug @@ -19,7 +19,7 @@ library | lines | files | schema | language | other notes [node-sql][3] | 2600 | 59 | schema | javascript | [mongo-sql][4] | 1700 | 49 | schemaless | javascript | [gesundheit][5] | 1600 | 21 | schemaless | coffeescript | uses Any-DB to wrap the DB driver -[sql-bricks][6] | 750 | 1 | schemaless | javascript | +[sql-bricks][6] | 800 | 1 | schemaless | javascript | [1]: https://github.com/tgriesser/knex [2]: https://github.com/hiddentao/squel @@ -28,6 +28,10 @@ library | lines | files | schema | language | other notes [5]: https://github.com/BetSmartMedia/gesundheit [6]: https://github.com/CSNW/sql-bricks +# Related Libraries + +[pg-bricks](https://github.com/Suor/pg-bricks) adds postgres connections, transactions, query execution and data accessors on top of SQLBricks. + # Use SQLBricks' only dependency is [Underscore.js](http://underscorejs.org/). From 2483d1fba6ce68b4975d531e6d1c39c0aff4717d Mon Sep 17 00:00:00 2001 From: Peter Rust Date: Fri, 29 Aug 2014 06:24:40 -0700 Subject: [PATCH 006/119] 0.14.0 --- package.json | 103 ++++++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 70c6482..9408397 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,57 @@ -{ - "name": "sql-bricks", - "version": "0.13.0", - "author": "Peter Rust ", - "description": "Transparent, Schemaless SQL Generation", - "homepage": "http://csnw.github.io/sql-bricks", - "bugs": "https://github.com/CSNW/sql-bricks/issues", - "scripts": { - "prepublish": "node tests/gen-tests.js", - "test": "mocha tests/tests.js tests/doctests.js" - }, - "main": "sql-bricks.js", - "repository": { - "type": "git", - "url": "https://github.com/CSNW/sql-bricks.git" - }, - "keywords": [ - "sql", "generation", "generate", "query", "pg", "postgres", "sqlite", "builder", "select", "insert", "update", "delete" - ], - "engines": { - "node": "*" - }, - "dependencies": { - "underscore": "1.4.x" - }, - "devDependencies": { - "mocha": "1.13.x" - }, - "license": "MIT", - "testling": { - "html": "browser-tests.html", - "browsers": [ - "chrome/29.0", - "iexplore/8.0", - "iexplore/9.0", - "iexplore/10.0", - "firefox/4.0", - "firefox/6.0", - "firefox/24.0", - "safari/6.0", - "iphone/6.0", - "ipad/6.0", - "android-browser/4.2" - ] - } -} +{ + "name": "sql-bricks", + "version": "0.14.0", + "author": "Peter Rust ", + "description": "Transparent, Schemaless SQL Generation", + "homepage": "http://csnw.github.io/sql-bricks", + "bugs": "https://github.com/CSNW/sql-bricks/issues", + "scripts": { + "prepublish": "node tests/gen-tests.js", + "test": "mocha tests/tests.js tests/doctests.js" + }, + "main": "sql-bricks.js", + "repository": { + "type": "git", + "url": "https://github.com/CSNW/sql-bricks.git" + }, + "keywords": [ + "sql", + "generation", + "generate", + "query", + "pg", + "postgres", + "sqlite", + "builder", + "select", + "insert", + "update", + "delete" + ], + "engines": { + "node": "*" + }, + "dependencies": { + "underscore": "1.4.x" + }, + "devDependencies": { + "mocha": "1.13.x" + }, + "license": "MIT", + "testling": { + "html": "browser-tests.html", + "browsers": [ + "chrome/29.0", + "iexplore/8.0", + "iexplore/9.0", + "iexplore/10.0", + "firefox/4.0", + "firefox/6.0", + "firefox/24.0", + "safari/6.0", + "iphone/6.0", + "ipad/6.0", + "android-browser/4.2" + ] + } +} From d875f84625808a61cdbc555e38a56d41ca0dd946 Mon Sep 17 00:00:00 2001 From: Peter Rust Date: Fri, 19 Sep 2014 11:25:11 -0700 Subject: [PATCH 007/119] First pass at dialect extension mechanism * Moved LIMIT ... OFFSET to a limit-offset extension (used by both sqlite/pg) * Moved OR (replace/abort/etc) to sqlite extension * Moved RETURNING & DELETE ... USING to postgres extension * Removed MINUS support (Oracle only; if someone wants to create an Oracle extension and maintain it in their own repo, they're welcome to) * Removed unnecessary layer of method-building functions --- browser-tests.html | 7 + index.html | 8 +- limit-offset.js | 46 + package.json | 4 +- postgres.js | 36 + readme.md | 6 +- sql-bricks.js | 1683 ++++++++++++++++++----------------- sqlite.js | 32 + tests/doctests.js | 26 +- tests/doctests.tmpl | 12 +- tests/limit-offset-tests.js | 72 ++ tests/postgres-tests.js | 61 ++ tests/sqlite-tests.js | 49 + tests/tests.js | 52 +- 14 files changed, 1182 insertions(+), 912 deletions(-) create mode 100644 limit-offset.js create mode 100644 postgres.js create mode 100644 sqlite.js create mode 100644 tests/limit-offset-tests.js create mode 100644 tests/postgres-tests.js create mode 100644 tests/sqlite-tests.js diff --git a/browser-tests.html b/browser-tests.html index 9a11784..349be5b 100644 --- a/browser-tests.html +++ b/browser-tests.html @@ -10,7 +10,14 @@ + + + + + + + - - - - - -