diff --git a/lib/sql.js b/lib/sql.js index 3148e320..aa0f6f9f 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -5,7 +5,6 @@ var Connector = require('./connector'); var debug = require('debug')('loopback:connector:sql'); var ParameterizedSQL = require('./parameterized-sql'); var Transaction = require('./transaction'); -var assign = require('./utils').assign; module.exports = SQLConnector; @@ -229,24 +228,15 @@ SQLConnector.prototype.tableEscaped = function(model) { * @param {String} property The property name * @returns {String} The escaped column name */ -SQLConnector.prototype.columnEscaped = function(model, property) { - return this.escapeName(this.column(model, property)); -}; - -/** - * Get the escaped qualified column name (table.column for join) - * @param {String} model The model name - * @param {String} property The property name - * @returns {String} The escaped column name - */ -SQLConnector.prototype.qualifiedColumnEscaped = function(model, property) { - var table = this.tableEscaped(model); - var index = table.indexOf('.'); - if (index !== -1) { - // Remove the schema name - table = table.substring(index); +SQLConnector.prototype.columnEscaped = function(model, property, relName) { + var prefix = ''; + if (!relName) { + prefix = this.tableEscaped(model); + } else { + prefix = this.escapeName(relName); } - return table + '.' + this.escapeName(this.column(model, property)); + + return prefix + '.' + this.escapeName(this.column(model, property)); }; /*! @@ -749,7 +739,7 @@ SQLConnector.prototype.buildExpression = function(columnName, operator, columnVa * @returns {ParameterizedSQL} * @private */ -SQLConnector.prototype._buildWhere = function(model, where) { +SQLConnector.prototype._buildWhere = function(model, where, asName) { if (!where) { return new ParameterizedSQL(''); } @@ -776,10 +766,64 @@ SQLConnector.prototype._buildWhere = function(model, where) { var clauses = where[key]; if (Array.isArray(clauses)) { for (var i = 0, n = clauses.length; i < n; i++) { + var relationClauses = []; + for (var ck in clauses[i]) { + if (ck in relations) { + // Build a where clause with relation model + var relClauses = []; + var rel = relations[ck]; + var relModel = rel.modelTo; + var relWhere = clauses[i][ck]; + var lastRelWhere = relWhere; + var loopRelName = ''; + + do { + lastRelWhere = relWhere; + if (loopRelName) { + loopRelName = loopRelName + '_' + rel.name; + } else { + loopRelName = rel.name; + } + var loopRelModel = relModel; + + for (var wk in lastRelWhere) { + if (wk in relModel.relations) { + rel = relModel.relations[wk]; + relModel = rel.modelTo; + relWhere = lastRelWhere[wk]; + } else { + var w = {}; + w[wk] = lastRelWhere[wk]; + relClauses.push(self._buildWhere(loopRelModel.definition.name, w, loopRelName)); + } + } + } while(lastRelWhere !== relWhere); + + if (relClauses.length) { + relationClauses = relationClauses.concat(relClauses); + } + } + } + var stmtForClause = self._buildWhere(model, clauses[i]); - stmtForClause.sql = '(' + stmtForClause.sql + ')'; branchParams = branchParams.concat(stmtForClause.params); - branches.push(stmtForClause.sql); + + if (relationClauses.length) { + var relationSql = []; + + for (var y = 0; y < relationClauses.length; y++) { + branchParams = branchParams.concat(relationClauses[y].params); + relationSql.push(relationClauses[y].sql); + } + + if (stmtForClause.sql.length) { + stmtForClause.sql = stmtForClause.sql + ' AND ' + relationSql.join(' AND '); + } else { + stmtForClause.sql = relationSql.join(' AND '); + } + } + + branches.push('(' + stmtForClause.sql + ')'); } stmt.merge({ sql: branches.join(' ' + key.toUpperCase() + ' '), @@ -790,7 +834,7 @@ SQLConnector.prototype._buildWhere = function(model, where) { } // The value is not an array, fall back to regular fields } - var columnName = self.columnEscaped(model, key); + var columnName = self.columnEscaped(model, key, asName); var expression = where[key]; var columnValue; var sqlExp; @@ -889,12 +933,25 @@ SQLConnector.prototype.buildOrderBy = function(model, order) { } else { // Column name is in the format: relationName.columnName var colSplit = t[0].split('.'); - // Find the name of the relation's model ... - var modelDef = this.getModelDefinition(model); - var relation = modelDef.model.relations[colSplit[0]]; - var colModel = relation.modelTo.definition.name; - // ... and escape them - colName = self.columnEscaped(colModel, colSplit[1]); + var prevModel = model; + var relName = ''; + for (var y = 0; y < colSplit.length; y++) { + if (y >= colSplit.length - 1) { + break; + } + // Find the name of the relation's model ... + var modelDef = this.getModelDefinition(prevModel); + var relation = modelDef.model.relations[colSplit[y]]; + var colModel = relation.modelTo.definition.name; + prevModel = colModel; + if (relName) { + relName = relName + '_' + relation.name; + } else { + relName = relation.name; + } + // ... and escape them + colName = self.columnEscaped(colModel, colSplit[colSplit.length - 1], relName); + } } if (t.length === 1) { @@ -1017,23 +1074,82 @@ SQLConnector.prototype.buildColumnNames = function(model, filter) { }; /** - * Build a SQL SELECT statement + * Prepare filter data form buildSelect and buildCount * @param {String} model Model name * @param {Object} filter Filter object * @param {Object} options Options object - * @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]} + * @returns {Object} filter Filter object */ -SQLConnector.prototype.buildSelect = function(model, filter, options) { - options = options || {}; +SQLConnector.prototype._prepareFilter = function (model, filter) { + var relations = this.getModelDefinition(model).model.relations; if (!filter.order) { var idNames = this.idNames(model); if (idNames && idNames.length) { filter.order = idNames; } + } else { + var order = filter.order; + if (!Array.isArray(order)) { + order = [ order ]; + } + + order.forEach(function (order) { + var nextWhere = filter.where = filter.where || {}; + var col = order.split(/[\s,]+/)[0].split('.'); + if (col.length <= 1) { + return; + } + + for (var i = 0; i < col.length; i++) { + if (i >= col.length -1) { + break; + } + nextWhere = nextWhere[col[i]] = nextWhere[col[i]] || {}; + } + }); } + if (filter.where) { + fixWhere(filter.where, relations, filter.where); + } + + return filter; + + // Fix and/or relations + // Make sure if a relation is targetted in a `and`/`or` + // we create a empty where clause that will produce a join later + function fixWhere (where, relations, dest) { + if (Array.isArray(where)) { + where.forEach(function (where) { + fixWhere(where, relations, dest); + }); + } else { + for (var key in where) { + if (key === 'and' || key === 'or') { + fixWhere(where[key], relations, dest); + } else if (key in relations) { + dest[key] = dest[key] || {}; + fixWhere(where[key], relations[key].modelTo.relations, dest[key]); + } + } + } + } +}; + +/** + * Build a SQL SELECT statement + * @param {String} model Model name + * @param {Object} filter Filter object + * @param {Object} options Options object + * @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]} + */ +SQLConnector.prototype.buildSelect = function(model, filter, options) { + options = options || {}; + filter = this._prepareFilter(model, filter); + var haveRelationFilters = this.hasRelationClause(model, filter.where); + var distinct = haveRelationFilters ? 'DISTINCT ' : ''; var selectStmt = new ParameterizedSQL('SELECT ' + distinct + @@ -1077,34 +1193,50 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { * @param {object} where An object for the where conditions * @returns {ParameterizedSQL} The SQL INNER JOIN clauses */ -SQLConnector.prototype.buildJoins = function(model, where) { +SQLConnector.prototype.buildJoins = function(model, where, prevRelName) { var modelDef = this.getModelDefinition(model); var relations = modelDef.model.relations; var stmt = new ParameterizedSQL('', []); var self = this; - var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) { + var buildOneToMany = function buildOneToMany(relName, modelFrom, keyFrom, modelTo, keyTo, joinWhere, + forcePrevRelName) { var ds1 = self.getDataSource(modelFrom); var ds2 = self.getDataSource(modelTo); assert(ds1 === ds2, 'Model ' + modelFrom + ' and ' + modelTo + ' must be attached to the same datasource'); + var modelToEscaped = self.tableEscaped(modelTo); - var innerFilter = assign({}, filter); - var innerIdField = {}; - innerIdField[keyTo] = true; - innerFilter.fields = assign({}, innerFilter.fields, innerIdField); + var relModel = self.getModelDefinition(modelTo); + var relRelations = relModel.model.relations; + var joinType = 'LEFT'; + + var condition = new ParameterizedSQL( + 'ON ' + + self.columnEscaped(modelFrom, keyFrom, forcePrevRelName || prevRelName) + '=' + + self.columnEscaped(modelTo, keyTo, relName) + ); + + if (joinWhere) { + for (var key in joinWhere) { + if (key in relRelations) continue; + + joinType = 'INNER'; + break; + } - var condition = self.qualifiedColumnEscaped(modelFrom, keyFrom) + '=' + - self.qualifiedColumnEscaped(modelTo, keyTo); + var innerWhere = self._buildWhere(modelTo, joinWhere, relName); + if (innerWhere.sql.length) { + condition.merge(['AND', innerWhere]); + } + } - var innerSelect = self.buildSelect(modelTo, innerFilter, { - skipParameterize: true - }); + var join = joinType + ' JOIN ' + modelToEscaped; + join += ' AS ' + self.escapeName(relName); - return new ParameterizedSQL('INNER JOIN (', []) - .merge(innerSelect) - .merge(') AS ' + modelToEscaped) - .merge('ON ' + condition); + return new ParameterizedSQL(join, []) + .merge(condition) + .merge(self.buildJoins(modelTo, joinWhere, relName)); }; for (var key in where) { @@ -1114,21 +1246,24 @@ SQLConnector.prototype.buildJoins = function(model, where) { var keyFrom = rel.keyFrom; var modelTo = rel.modelTo.definition.name; var keyTo = rel.keyTo; + var relName = rel.name; + if (prevRelName) { + relName = prevRelName + '_' + relName; + } var join; if (!rel.modelThrough) { // 1:n relation - join = buildOneToMany(model, keyFrom, modelTo, keyTo, where[key]); + join = buildOneToMany(relName, model, keyFrom, modelTo, keyTo, where[key]); } else { // n:m relation var modelThrough = rel.modelThrough.definition.name; var keyThrough = rel.keyThrough; var modelToKey = rel.modelTo.definition.idName(); - var innerFilter = {fields: {}}; - innerFilter.fields[keyThrough] = true; - var joinInner = buildOneToMany(model, keyFrom, modelThrough, keyTo, innerFilter); - join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key]); + var joinInner = buildOneToMany(relName + '_though', model, keyFrom, modelThrough, keyTo); + join = buildOneToMany(relName, modelThrough, keyThrough, modelTo, modelToKey, where[key], + relName + '_though'); join = joinInner.merge(join); } stmt.merge(join); @@ -1268,6 +1403,7 @@ SQLConnector.prototype.hasRelationClause = function(model, where) { * @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]} */ SQLConnector.prototype.buildCount = function(model, where, options) { + where = this._prepareFilter(model, { where: where }).where; var haveRelationFilters = this.hasRelationClause(model, where); var count = 'count(*)'; diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index 37e0e577..00000000 --- a/lib/utils.js +++ /dev/null @@ -1,18 +0,0 @@ -var _hasOwnProp = Object.prototype.hasOwnProperty; - -/** - * Object.assign polyfill - */ -var assign = Object.assign || function(target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - for (var key in source) { - if (_hasOwnProp.call(source, key)) { - target[key] = source[key]; - } - } - } - return target; -}; - -exports.assign = assign; diff --git a/test/sql.test.js b/test/sql.test.js index cfc2ca39..076ddb79 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -108,11 +108,6 @@ describe('sql connector', function() { it('should find escaped column name', function() { var column = connector.columnEscaped('customer', 'vip'); - expect(column).to.eql('`VIP`'); - }); - - it('should find escaped qualified column name', function() { - var column = connector.qualifiedColumnEscaped('customer', 'vip'); expect(column).to.eql('`CUSTOMER`.`VIP`'); }); @@ -124,7 +119,7 @@ describe('sql connector', function() { it('builds where', function() { var where = connector.buildWhere('customer', {name: 'John'}); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME`=?', + sql: 'WHERE `CUSTOMER`.`NAME`=?', params: ['John'] }); }); @@ -132,7 +127,7 @@ describe('sql connector', function() { it('builds where with null', function() { var where = connector.buildWhere('customer', {name: null}); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` IS NULL', + sql: 'WHERE `CUSTOMER`.`NAME` IS NULL', params: [] }); }); @@ -140,7 +135,7 @@ describe('sql connector', function() { it('builds where with inq', function() { var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` IN (?,?)', + sql: 'WHERE `CUSTOMER`.`NAME` IN (?,?)', params: ['John', 'Mary'] }); }); @@ -149,7 +144,7 @@ describe('sql connector', function() { var where = connector.buildWhere('customer', {or: [{name: 'John'}, {name: 'Mary'}]}); expect(where.toJSON()).to.eql({ - sql: 'WHERE (`NAME`=?) OR (`NAME`=?)', + sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`CUSTOMER`.`NAME`=?)', params: ['John', 'Mary'] }); }); @@ -158,7 +153,7 @@ describe('sql connector', function() { var where = connector.buildWhere('customer', {and: [{name: 'John'}, {vip: true}]}); expect(where.toJSON()).to.eql({ - sql: 'WHERE (`NAME`=?) AND (`VIP`=?)', + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`CUSTOMER`.`VIP`=?)', params: ['John', true] }); }); @@ -170,7 +165,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: ['^J'] }); }); @@ -182,7 +177,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: ['^J/i'] }); }); @@ -194,7 +189,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [/^J/] }); }); @@ -206,7 +201,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [/^J/i] }); }); @@ -218,7 +213,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [/^J/] }); }); @@ -230,7 +225,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [new RegExp(/^J/i)] }); }); @@ -239,31 +234,31 @@ describe('sql connector', function() { var where = connector.buildWhere('customer', {and: [{name: 'John'}, {or: [{vip: true}, {address: null}]}]}); expect(where.toJSON()).to.eql({ - sql: 'WHERE (`NAME`=?) AND ((`VIP`=?) OR ' + - '(`ADDRESS` IS NULL))', + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND ((`CUSTOMER`.`VIP`=?) OR ' + + '(`CUSTOMER`.`ADDRESS` IS NULL))', params: ['John', true] }); }); it('builds order by with one field', function() { var orderBy = connector.buildOrderBy('customer', 'name'); - expect(orderBy).to.eql('ORDER BY `NAME`'); + expect(orderBy).to.eql('ORDER BY `CUSTOMER`.`NAME`'); }); it('builds order by with two fields', function() { var orderBy = connector.buildOrderBy('customer', ['name', 'vip']); - expect(orderBy).to.eql('ORDER BY `NAME`,`VIP`'); + expect(orderBy).to.eql('ORDER BY `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`'); }); it('builds order by with two fields and dirs', function() { var orderBy = connector.buildOrderBy('customer', ['name ASC', 'vip DESC']); - expect(orderBy).to.eql('ORDER BY `NAME` ASC,`VIP` DESC'); + expect(orderBy).to.eql('ORDER BY `CUSTOMER`.`NAME` ASC,`CUSTOMER`.`VIP` DESC'); }); it('builds fields for columns', function() { var fields = connector.buildFields('customer', {name: 'John', vip: true, unknown: 'Random'}); - expect(fields.names).to.eql(['`NAME`', '`VIP`']); + expect(fields.names).to.eql(['`CUSTOMER`.`NAME`', '`CUSTOMER`.`VIP`']); expect(fields.columnValues[0].toJSON()).to.eql( {sql: '?', params: ['John']}); expect(fields.columnValues[1].toJSON()).to.eql( @@ -274,7 +269,7 @@ describe('sql connector', function() { var fields = connector.buildFieldsForUpdate('customer', {name: 'John', vip: true}); expect(fields.toJSON()).to.eql({ - sql: 'SET `VIP`=?', + sql: 'SET `CUSTOMER`.`VIP`=?', params: [true] }); }); @@ -283,36 +278,36 @@ describe('sql connector', function() { var fields = connector.buildFieldsForUpdate('customer', {name: 'John', vip: true}, false); expect(fields.toJSON()).to.eql({ - sql: 'SET `NAME`=?,`VIP`=?', + sql: 'SET `CUSTOMER`.`NAME`=?,`CUSTOMER`.`VIP`=?', params: ['John', true] }); }); it('builds column names for SELECT', function() { var cols = connector.buildColumnNames('customer'); - expect(cols).to.eql('`NAME`,`VIP`,' + - '`ADDRESS`,`FAVORITE_STORE`'); + expect(cols).to.eql('`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE`'); }); it('builds column names with true fields filter for SELECT', function() { var cols = connector.buildColumnNames('customer', {fields: {name: true}}); - expect(cols).to.eql('`NAME`'); + expect(cols).to.eql('`CUSTOMER`.`NAME`'); }); it('builds column names with false fields filter for SELECT', function() { var cols = connector.buildColumnNames('customer', {fields: {name: false}}); - expect(cols).to.eql('`VIP`,`ADDRESS`,`FAVORITE_STORE`'); + expect(cols).to.eql('`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE`'); }); it('builds column names with array fields filter for SELECT', function() { var cols = connector.buildColumnNames('customer', {fields: ['name']}); - expect(cols).to.eql('`NAME`'); + expect(cols).to.eql('`CUSTOMER`.`NAME`'); }); it('builds DELETE', function() { var sql = connector.buildDelete('customer', {name: 'John'}); expect(sql.toJSON()).to.eql({ - sql: 'DELETE FROM `CUSTOMER` WHERE `NAME`=$1', + sql: 'DELETE FROM `CUSTOMER` WHERE `CUSTOMER`.`NAME`=$1', params: ['John'] }); }); @@ -320,7 +315,7 @@ describe('sql connector', function() { it('builds UPDATE', function() { var sql = connector.buildUpdate('customer', {name: 'John'}, {vip: false}); expect(sql.toJSON()).to.eql({ - sql: 'UPDATE `CUSTOMER` SET `VIP`=$1 WHERE `NAME`=$2', + sql: 'UPDATE `CUSTOMER` SET `CUSTOMER`.`VIP`=$1 WHERE `CUSTOMER`.`NAME`=$2', params: [false, 'John'] }); }); @@ -329,9 +324,9 @@ describe('sql connector', function() { var sql = connector.buildSelect('customer', {order: 'name', limit: 5, where: {name: 'John'}}); expect(sql.toJSON()).to.eql({ - sql: 'SELECT `NAME`,`VIP`,`ADDRESS`,' + - '`FAVORITE_STORE` FROM `CUSTOMER` WHERE `NAME`=$1 ' + - 'ORDER BY `NAME` LIMIT 5', + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` FROM `CUSTOMER` WHERE `CUSTOMER`.`NAME`=$1 ' + + 'ORDER BY `CUSTOMER`.`NAME` LIMIT 5', params: ['John'] }); }); @@ -339,38 +334,51 @@ describe('sql connector', function() { it('builds INSERT', function() { var sql = connector.buildInsert('customer', {name: 'John', vip: true}); expect(sql.toJSON()).to.eql({ - sql: 'INSERT INTO `CUSTOMER`(`NAME`,`VIP`) VALUES($1,$2)', + sql: 'INSERT INTO `CUSTOMER`(`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`) VALUES($1,$2)', params: ['John', true] }); }); it('builds INNER JOIN', function () { - var sql = connector.buildJoins('customer', {orders: {where: {id: 10}}}); + var sql = connector.buildJoins('customer', {orders: {id: 10}}); expect(sql.toJSON()).to.eql({ - sql: 'INNER JOIN ( SELECT `CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`ID`=? ORDER BY `ID` ) AS `ORDER` ON ' + - '`CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME`', + sql: + 'INNER JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` AND `orders`.`ID`=? ', params: [10] }); }); + it('builds LEFT JOIN', function () { + var sql = connector.buildJoins('customer', {orders: {}}); + expect(sql.toJSON()).to.eql({ + sql: + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ', + params: [] + }); + }); + it('builds SELECT with INNER JOIN (1:n relation)', function () { var sql = connector.buildSelect('customer', { where: { orders: { - where: { - date: {between: ['2015-01-01', '2015-01-31']} - } + date: {between: ['2015-01-01', '2015-01-31']} } } }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `NAME`,`VIP`,' + - '`ADDRESS`,`FAVORITE_STORE` FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT `CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`DATE` BETWEEN $1 AND $2 ORDER BY `ID` ) AS `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ORDER BY `NAME`', + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'INNER JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'AND `orders`.`DATE` BETWEEN $1 AND $2 ' + + 'ORDER BY `CUSTOMER`.`NAME`', params: ['2015-01-01', '2015-01-31'] }); }); @@ -379,44 +387,417 @@ describe('sql connector', function() { var sql = connector.buildSelect('store', { where: { customers: { - where: { - vip: true - } + vip: true } } }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `ID`,`STATE` FROM `STORE` INNER JOIN' + - ' ( SELECT `CUSTOMER_NAME`,`STORE_ID` FROM `ORDER` ' + - 'ORDER BY `ID` ) AS `ORDER` ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + - 'INNER JOIN ( SELECT `NAME` FROM `CUSTOMER` WHERE ' + - '`VIP`=$1 ORDER BY `NAME` ) AS `CUSTOMER` ON ' + - '`ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ORDER BY `ID`', + sql: + 'SELECT DISTINCT `STORE`.`ID`,' + + '`STORE`.`STATE` ' + + 'FROM `STORE` ' + + 'LEFT JOIN `ORDER` AS `customers_though` ' + + 'ON `STORE`.`ID`=`customers_though`.`STORE_ID` ' + + 'INNER JOIN `CUSTOMER` AS `customers` ' + + 'ON `customers_though`.`CUSTOMER_NAME`=`customers`.`NAME` ' + + 'AND `customers`.`VIP`=$1 ' + + 'ORDER BY `STORE`.`ID`', params: [true] }); }); - it('builds SELECT with INNER JOIN and order by relation columns', function () { + it('builds SELECT with JOIN with or', function () { + var sql = connector.buildSelect('customer', { + where: { + or: [{ + orders: { + date: {between: ['2015-01-01', '2015-01-31']} + } + }, { + orders: { + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + } + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'WHERE (' + + '`orders`.`DATE` BETWEEN $1 AND $2' + + ') OR (' + + '`orders`.`DATE` BETWEEN $3 AND $4' + + ') ' + + 'ORDER BY `CUSTOMER`.`NAME`', + params: [ + '2015-01-01', + '2015-01-31', + '2015-02-01', + '2015-02-31' + ] + }); + }); + + it('builds SELECT with JOIN with and', function () { + var sql = connector.buildSelect('customer', { + where: { + and: [{ + orders: { + date: {between: ['2015-01-01', '2015-01-31']} + } + }, { + orders: { + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + } + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'WHERE (' + + '`orders`.`DATE` BETWEEN $1 AND $2' + + ') AND (' + + '`orders`.`DATE` BETWEEN $3 AND $4' + + ') ' + + 'ORDER BY `CUSTOMER`.`NAME`', + params: [ + '2015-01-01', + '2015-01-31', + '2015-02-01', + '2015-02-31' + ] + }); + }); + + it('builds SELECT with JOIN with or and nested relation', function () { + var sql = connector.buildSelect('customer', { + where: { + or: [{ + orders: { + store: { + state: 'NY' + } + } + }, { + orders: { + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + } + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'LEFT JOIN `STORE` AS `orders_store` ' + + 'ON `orders`.`STORE_ID`=`orders_store`.`ID` ' + + 'WHERE (`orders_store`.`STATE`=$1) ' + + 'OR (`orders`.`DATE` BETWEEN $2 AND $3) ' + + 'ORDER BY `CUSTOMER`.`NAME`', + params: [ + 'NY', + '2015-02-01', + '2015-02-31' + ] + }); + }); + + it('builds SELECT with JOIN with or and nested relation with non relation search', function () { + var sql = connector.buildSelect('customer', { + where: { + or: [{ + orders: { + date: {between: ['2015-01-01', '2015-01-31']}, + store: { + state: 'NY' + } + } + }, { + orders: { + store: { + state: 'NY' + }, + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + } + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'LEFT JOIN `STORE` AS `orders_store` ' + + 'ON `orders`.`STORE_ID`=`orders_store`.`ID` ' + + 'WHERE (`orders`.`DATE` BETWEEN $1 AND $2 AND `orders_store`.`STATE`=$3) ' + + 'OR (`orders`.`DATE` BETWEEN $4 AND $5 AND `orders_store`.`STATE`=$6) ' + + 'ORDER BY `CUSTOMER`.`NAME`', + params: [ + '2015-01-01', + '2015-01-31', + 'NY', + '2015-02-01', + '2015-02-31', + 'NY' + ] + }); + }); + + it('builds SELECT with JOIN with nested or', function () { + var sql = connector.buildSelect('customer', { + where: { + or: [{ + /*jshint camelcase:false */ + favorite_store: { + state: 'NY', + or: [{ + orders: { + date: {between: ['2015-01-01', '2015-01-31']} + } + }, { + orders: { + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + } + }, { + /*jshint camelcase:false */ + favorite_store: { + state: 'baz', + and: [{ + orders: { + date: {between: ['2015-01-01', '2015-01-31']} + } + }, { + orders: { + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + } + }] + } + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `STORE` AS `favorite_store` ' + + 'ON `CUSTOMER`.`FAVORITE_STORE`=`favorite_store`.`ID` ' + + 'LEFT JOIN `ORDER` AS `favorite_store_orders` ' + + 'ON `favorite_store`.`ID`=`favorite_store_orders`.`STORE_ID` ' + + 'WHERE (' + + '`favorite_store`.`STATE`=$1 AND (' + + '`orders`.`DATE` BETWEEN $2 AND $3' + + ') ' + + 'OR (`orders`.`DATE` BETWEEN $4 AND $5)' + + ') OR (' + + '`favorite_store`.`STATE`=$6 ' + + 'AND (`orders`.`DATE` BETWEEN $7 AND $8) ' + + 'AND (`orders`.`DATE` BETWEEN $9 AND $10)' + + ') ' + + 'ORDER BY `CUSTOMER`.`NAME`', + params: [ + 'NY', + '2015-01-01', + '2015-01-31', + '2015-02-01', + '2015-02-31', + 'baz', + '2015-01-01', + '2015-01-31', + '2015-02-01', + '2015-02-31' + ] + }); + }); + + it('builds SELECT with JOIN and order by relation columns', function () { + var sql = connector.buildSelect('order', { + order: ['customer.vip DESC', 'customer.name ASC'] + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'ORDER BY `customer`.`VIP` DESC,' + + '`customer`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with JOIN and order by relation columns with where but no conditions', function () { + var sql = connector.buildSelect('order', { + where: { + customer: {} + }, + order: ['customer.vip DESC', 'customer.name ASC'] + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'ORDER BY `customer`.`VIP` DESC,' + + '`customer`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with JOIN and order by relation columns with any where', function () { + var sql = connector.buildSelect('order', { + order: ['customer.vip DESC', 'customer.name ASC'] + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'ORDER BY `customer`.`VIP` DESC,' + + '`customer`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with JOIN and order by nested relation columns', function () { + var sql = connector.buildSelect('order', { + order: ['customer.favorite_store.state DESC', 'customer.name ASC'] + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'LEFT JOIN `STORE` AS `customer_favorite_store` ' + + 'ON `customer`.`FAVORITE_STORE`=`customer_favorite_store`.`ID` ' + + 'ORDER BY `customer_favorite_store`.`STATE` DESC,' + + '`customer`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with INNER JOIN and order by nested relation columns with where', function () { var sql = connector.buildSelect('order', { where: { + date: {between: ['2015-01-01', '2015-01-31']}, customer: { - fields: { - 'name': true, - 'vip': true + name: 'foo', + /*jshint camelcase:false */ + favorite_store: { + state: 'NY' } } }, - order: ['customer.vip DESC', 'customer.name ASC'] + order: ['customer.favorite_store.state DESC', 'customer.name ASC'] }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `ID`,`DATE`,`CUSTOMER_NAME`,' + - '`STORE_ID` FROM `ORDER` INNER JOIN ( SELECT `NAME`,' + - '`VIP` FROM `CUSTOMER` ORDER BY `NAME` ) AS `CUSTOMER`' + - ' ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ORDER BY ' + - '`VIP` DESC,`NAME` ASC', - params: [] + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'INNER JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'AND `customer`.`NAME`=$1 ' + + 'INNER JOIN `STORE` AS `customer_favorite_store` ' + + 'ON `customer`.`FAVORITE_STORE`=`customer_favorite_store`.`ID` ' + + 'AND `customer_favorite_store`.`STATE`=$2 ' + + 'WHERE `ORDER`.`DATE` BETWEEN $3 AND $4 ' + + 'ORDER BY `customer_favorite_store`.`STATE` DESC,' + + '`customer`.`NAME` ASC', + params: [ + 'foo', + 'NY', + '2015-01-01', + '2015-01-31' + ] + }); + }); + + it('builds SELECT with INNER JOIN and order by nested relation columns with partial where', function () { + var sql = connector.buildSelect('order', { + where: { + date: {between: ['2015-01-01', '2015-01-31']}, + customer: { + name: 'foo' + } + }, + order: ['customer.favorite_store.state DESC', 'customer.name ASC'] + }); + + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'INNER JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'AND `customer`.`NAME`=$1 ' + + 'LEFT JOIN `STORE` AS `customer_favorite_store` ' + + 'ON `customer`.`FAVORITE_STORE`=`customer_favorite_store`.`ID` ' + + 'WHERE `ORDER`.`DATE` BETWEEN $2 AND $3 ' + + 'ORDER BY `customer_favorite_store`.`STATE` DESC,' + + '`customer`.`NAME` ASC', + params: [ + 'foo', + '2015-01-01', + '2015-01-31' + ] }); }); @@ -424,27 +805,29 @@ describe('sql connector', function() { var sql = connector.buildSelect('customer', { where: { orders: { - where: { - date: {between: ['2015-01-01', '2015-01-31']} - } + date: {between: ['2015-01-01', '2015-01-31']} }, /*jshint camelcase:false */ favorite_store: { - where: { - state: 'NY' - } + state: 'NY' } } }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `NAME`,`VIP`,' + - '`ADDRESS`,`FAVORITE_STORE` FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT `CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`DATE` BETWEEN $1 AND $2 ORDER BY `ID` ) AS `ORDER` ON ' + - '`CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` INNER JOIN ( SELECT `ID` ' + - 'FROM `STORE` WHERE `STATE`=$3 ORDER BY `ID` ) AS `STORE` ' + - 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ORDER BY `NAME`', + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'INNER JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'AND `orders`.`DATE` BETWEEN $1 AND $2 ' + + 'INNER JOIN `STORE` AS `favorite_store` ' + + 'ON `CUSTOMER`.`FAVORITE_STORE`=`favorite_store`.`ID` ' + + 'AND `favorite_store`.`STATE`=$3 ' + + 'ORDER BY `CUSTOMER`.`NAME`', params: ['2015-01-01', '2015-01-31', 'NY'] }); }); @@ -453,25 +836,25 @@ describe('sql connector', function() { var sql = connector.buildSelect('customer', { where: { orders: { - where: { - store: { - where: { - state: 'NY' - } - } + store: { + state: 'NY' } } } }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `NAME`,`VIP`,' + - '`ADDRESS`,`FAVORITE_STORE` FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT DISTINCT `CUSTOMER_NAME` FROM `ORDER` ' + - 'INNER JOIN ( SELECT `ID` FROM `STORE` WHERE `STATE`=$1 ' + - 'ORDER BY `ID` ) AS `STORE` ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + - 'ORDER BY `ID` ) AS `ORDER` ON `CUSTOMER`.`NAME`=`ORDER`.' + - '`CUSTOMER_NAME` ORDER BY `NAME`', + sql: + 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,'+ + '`CUSTOMER`.`FAVORITE_STORE` ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'INNER JOIN `STORE` AS `orders_store` ' + + 'ON `orders`.`STORE_ID`=`orders_store`.`ID` AND `orders_store`.`STATE`=$1 ' + + 'ORDER BY `CUSTOMER`.`NAME`', params: ['NY'] }); }); @@ -487,7 +870,7 @@ describe('sql connector', function() { it('builds count with WHERE', function() { var sql = connector.buildCount('customer', {name: 'John'}); expect(sql.toJSON()).to.eql({ - sql: 'SELECT count(*) as "cnt" FROM `CUSTOMER` WHERE `NAME`=$1', + sql: 'SELECT count(*) as "cnt" FROM `CUSTOMER` WHERE `CUSTOMER`.`NAME`=$1', params: ['John'] }); }); @@ -496,20 +879,53 @@ describe('sql connector', function() { var sql = connector.buildCount('customer', { name: 'John', orders: { - where: { - date: {between: ['2015-01-01', '2015-01-31']} - } + date: {between: ['2015-01-01', '2015-01-31']} } }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT count(DISTINCT `NAME`) as "cnt" FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT `CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`DATE` BETWEEN $1 AND $2 ORDER BY `ID` ) AS `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` WHERE `NAME`=$3', + sql: + 'SELECT count(DISTINCT `CUSTOMER`.`NAME`) as \"cnt\" ' + + 'FROM `CUSTOMER` ' + + 'INNER JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'AND `orders`.`DATE` BETWEEN $1 AND $2 ' + + 'WHERE `CUSTOMER`.`NAME`=$3', params: ['2015-01-01', '2015-01-31', 'John'] }); }); + it('builds count with WHERE and JOIN with or', function() { + var sql = connector.buildCount('customer', { + name: 'John', + or: [{ + orders: { + date: {between: ['2015-01-01', '2015-01-31']} + } + }, { + orders: { + date: {between: ['2015-02-01', '2015-02-31']} + } + }] + }); + expect(sql.toJSON()).to.eql({ + sql: + 'SELECT count(DISTINCT `CUSTOMER`.`NAME`) as \"cnt\" ' + + 'FROM `CUSTOMER` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + + 'WHERE `CUSTOMER`.`NAME`=$1 ' + + 'AND (`orders`.`DATE` BETWEEN $2 AND $3) ' + + 'OR (`orders`.`DATE` BETWEEN $4 AND $5)', + params: [ + 'John', + '2015-01-01', + '2015-01-31', + '2015-02-01', + '2015-02-31' + ] + }); + }); + it('normalizes a SQL statement from string', function() { var sql = 'SELECT * FROM `CUSTOMER`'; var stmt = new ParameterizedSQL(sql);