From 4a3a7ef7ab02de6e46953b74eeefc7d3e1c91b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mahieu?= Date: Sat, 27 Feb 2016 12:35:47 +0100 Subject: [PATCH 1/6] Refactor for simple inner join --- lib/sql.js | 57 ++++++---------- lib/utils.js | 18 ----- test/sql.test.js | 168 +++++++++++++++++++++++++++-------------------- 3 files changed, 115 insertions(+), 128 deletions(-) delete mode 100644 lib/utils.js diff --git a/lib/sql.js b/lib/sql.js index 3148e320..51444bc5 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; @@ -230,23 +229,7 @@ SQLConnector.prototype.tableEscaped = function(model) { * @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); - } - return table + '.' + this.escapeName(this.column(model, property)); + return this.tableEscaped(model) + '.' + this.escapeName(this.column(model, property)); }; /*! @@ -1083,28 +1066,30 @@ SQLConnector.prototype.buildJoins = function(model, where) { var stmt = new ParameterizedSQL('', []); var self = this; - var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) { + var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, joinWhere) { 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 condition = self.qualifiedColumnEscaped(modelFrom, keyFrom) + '=' + - self.qualifiedColumnEscaped(modelTo, keyTo); + var condition = new ParameterizedSQL( + 'ON ' + + self.columnEscaped(modelFrom, keyFrom) + '=' + + self.columnEscaped(modelTo, keyTo) + ); - var innerSelect = self.buildSelect(modelTo, innerFilter, { - skipParameterize: true - }); + if (joinWhere) { + var innerWhere = self._buildWhere(modelTo, joinWhere); + if (innerWhere.sql.length) { + condition.merge(['AND', innerWhere]); + } + } - return new ParameterizedSQL('INNER JOIN (', []) - .merge(innerSelect) - .merge(') AS ' + modelToEscaped) - .merge('ON ' + condition); + return new ParameterizedSQL('INNER JOIN ' + modelToEscaped, []) + .merge(condition) + .merge(self.buildJoins(modelTo, joinWhere)); }; for (var key in where) { @@ -1118,17 +1103,15 @@ SQLConnector.prototype.buildJoins = function(model, where) { var join; if (!rel.modelThrough) { // 1:n relation - join = buildOneToMany(model, keyFrom, modelTo, keyTo, where[key]); + join = buildOneToMany(model, keyFrom, modelTo, keyTo, where[key].where); } 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(model, keyFrom, modelThrough, keyTo); + join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key].where); join = joinInner.merge(join); } stmt.merge(join); 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..645226c6 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,7 +334,7 @@ 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] }); }); @@ -347,9 +342,9 @@ describe('sql connector', function() { it('builds INNER JOIN', function () { var sql = connector.buildJoins('customer', {orders: {where: {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` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` AND `ORDER`.`ID`=? ', params: [10] }); }); @@ -366,11 +361,16 @@ describe('sql connector', function() { }); 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` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'AND `ORDER`.`DATE` BETWEEN $1 AND $2 ' + + 'ORDER BY `CUSTOMER`.`NAME`', params: ['2015-01-01', '2015-01-31'] }); }); @@ -387,12 +387,16 @@ describe('sql connector', function() { }); 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` ' + + 'INNER JOIN `ORDER` ' + + 'ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + + 'INNER JOIN `CUSTOMER` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'AND `CUSTOMER`.`VIP`=$1 ' + + 'ORDER BY `STORE`.`ID`', params: [true] }); }); @@ -411,11 +415,16 @@ describe('sql connector', function() { }); 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', + sql: + 'SELECT DISTINCT `ORDER`.`ID`,' + + '`ORDER`.`DATE`,' + + '`ORDER`.`CUSTOMER_NAME`,' + + '`ORDER`.`STORE_ID` ' + + 'FROM `ORDER` ' + + 'INNER JOIN `CUSTOMER` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'ORDER BY `CUSTOMER`.`VIP` DESC,' + + '`CUSTOMER`.`NAME` ASC', params: [] }); }); @@ -438,13 +447,19 @@ describe('sql connector', function() { }); 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` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'AND `ORDER`.`DATE` BETWEEN $1 AND $2 ' + + 'INNER JOIN `STORE` ' + + 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + 'AND `STORE`.`STATE`=$3 ' + + 'ORDER BY `CUSTOMER`.`NAME`', params: ['2015-01-01', '2015-01-31', 'NY'] }); }); @@ -465,13 +480,17 @@ describe('sql connector', function() { }); 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` ' + + 'INNER JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'INNER JOIN `STORE` ' + + 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` AND `STORE`.`STATE`=$1 ' + + 'ORDER BY `CUSTOMER`.`NAME`', params: ['NY'] }); }); @@ -487,7 +506,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'] }); }); @@ -502,10 +521,13 @@ describe('sql connector', function() { } }); 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` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'AND `ORDER`.`DATE` BETWEEN $1 AND $2 ' + + 'WHERE `CUSTOMER`.`NAME`=$3', params: ['2015-01-01', '2015-01-31', 'John'] }); }); From 0b3dd515f3c1d774f26d861b87b04e72ab4e852f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mahieu?= Date: Sat, 27 Feb 2016 13:17:04 +0100 Subject: [PATCH 2/6] Allow sorting on relation properties without where --- lib/sql.js | 41 ++++++++++++-- test/sql.test.js | 145 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index 51444bc5..b8beeb07 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -872,12 +872,19 @@ 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; + 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; + // ... and escape them + colName = self.columnEscaped(colModel, colSplit[colSplit.length - 1]); + } } if (t.length === 1) { @@ -1014,6 +1021,28 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { 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; + } + var curWhere = nextWhere[col[i]] = nextWhere[col[i]] || {}; + curWhere.where = curWhere.where || {}; + nextWhere = curWhere.where; + } + }); } var haveRelationFilters = this.hasRelationClause(model, filter.where); diff --git a/test/sql.test.js b/test/sql.test.js index 645226c6..f76a09cc 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -429,6 +429,151 @@ describe('sql connector', function() { }); }); + it('builds SELECT with INNER 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` ' + + 'INNER JOIN `CUSTOMER` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'ORDER BY `CUSTOMER`.`VIP` DESC,' + + '`CUSTOMER`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with INNER 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` ' + + 'INNER JOIN `CUSTOMER` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'ORDER BY `CUSTOMER`.`VIP` DESC,' + + '`CUSTOMER`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with INNER 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` ' + + 'INNER JOIN `CUSTOMER` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'INNER JOIN `STORE` ' + + 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + 'ORDER BY `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: { + where: { + name: 'foo', + /*jshint camelcase:false */ + favorite_store: { + where: { + state: 'NY' + } + } + } + } + }, + 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` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'AND `CUSTOMER`.`NAME`=$1 ' + + 'INNER JOIN `STORE` ' + + 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + 'AND `STORE`.`STATE`=$2 ' + + 'WHERE `ORDER`.`DATE` BETWEEN $3 AND $4 ' + + 'ORDER BY `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: { + where: { + 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` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + + 'AND `CUSTOMER`.`NAME`=$1 ' + + 'INNER JOIN `STORE` ' + + 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + 'WHERE `ORDER`.`DATE` BETWEEN $2 AND $3 ' + + 'ORDER BY `STORE`.`STATE` DESC,' + + '`CUSTOMER`.`NAME` ASC', + params: [ + 'foo', + '2015-01-01', + '2015-01-31' + ] + }); + }); + it('builds SELECT with multiple INNER JOIN', function () { var sql = connector.buildSelect('customer', { where: { From 653ad3c084c32af2e594dc3988b15811f7899ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mahieu?= Date: Sat, 27 Feb 2016 17:00:28 +0100 Subject: [PATCH 3/6] Fix support of `and` and `or` operator in where clause with relations --- lib/sql.js | 103 +++++++++++++++++-- test/sql.test.js | 258 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+), 9 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index b8beeb07..d95c5e4d 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -759,10 +759,56 @@ 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 relModel = relations[ck].modelTo; + var relWhere = clauses[i][ck]; + var lastRelWhere = relWhere; + + do { + lastRelWhere = relWhere; + var loopRelModel = relModel; + + for (var wk in lastRelWhere) { + if (wk in relModel.relations) { + relModel = relModel.relations[wk].modelTo; + relWhere = lastRelWhere[wk]; + } else { + var w = {}; + w[wk] = lastRelWhere[wk]; + relClauses.push(self._buildWhere(loopRelModel.definition.name, w)); + } + } + } 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() + ' '), @@ -1007,14 +1053,14 @@ 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); @@ -1038,14 +1084,52 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { if (i >= col.length -1) { break; } - var curWhere = nextWhere[col[i]] = nextWhere[col[i]] || {}; - curWhere.where = curWhere.where || {}; - nextWhere = curWhere.where; + 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] || {}; + dest[key].where = dest[key].where || {}; + fixWhere(where[key], relations[key].modelTo.relations, dest[key].where); + } + } + } + } +}; + +/** + * 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 + @@ -1280,6 +1364,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/test/sql.test.js b/test/sql.test.js index f76a09cc..e9123f84 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -401,6 +401,232 @@ describe('sql connector', function() { }); }); + it('builds SELECT with INNER 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` ' + + 'INNER JOIN `ORDER` ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2) ' + + 'OR (`ORDER`.`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 INNER 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` ' + + 'INNER JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2) ' + + 'AND (`ORDER`.`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 INNER 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` ' + + 'INNER JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'INNER JOIN `STORE` ' + + 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + + 'WHERE (`STORE`.`STATE`=$1) ' + + 'OR (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + + 'ORDER BY `CUSTOMER`.`NAME`', + params: [ + 'NY', + '2015-02-01', + '2015-02-31' + ] + }); + }); + + it('builds SELECT with INNER 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` ' + + 'INNER JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'INNER JOIN `STORE` ' + + 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + + 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2 AND `STORE`.`STATE`=$3) ' + + 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5 AND `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 INNER 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` ' + + 'INNER JOIN `STORE` ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + 'INNER JOIN `ORDER` ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + + 'WHERE (' + + '`STORE`.`STATE`=$1 AND (' + + '`ORDER`.`DATE` BETWEEN $2 AND $3' + + ') ' + + 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5)' + + ') OR (' + + '`STORE`.`STATE`=$6 ' + + 'AND (`ORDER`.`DATE` BETWEEN $7 AND $8) ' + + 'AND (`ORDER`.`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 INNER JOIN and order by relation columns', function () { var sql = connector.buildSelect('order', { where: { @@ -677,6 +903,38 @@ describe('sql connector', function() { }); }); + 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` ' + + 'INNER JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'WHERE `CUSTOMER`.`NAME`=$1 ' + + 'AND (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + + 'OR (`ORDER`.`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); From fb99d3f8a08bb4710818d7bdbe2ab18c160433ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mahieu?= Date: Mon, 29 Feb 2016 11:55:40 +0100 Subject: [PATCH 4/6] Only use INNER JOIN when there is a condition --- lib/sql.js | 12 +++++++- test/sql.test.js | 76 ++++++++++++++++++++++++++++-------------------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index d95c5e4d..d15c7bec 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -1186,6 +1186,9 @@ SQLConnector.prototype.buildJoins = function(model, where) { ' must be attached to the same datasource'); var modelToEscaped = self.tableEscaped(modelTo); + var relModel = self.getModelDefinition(modelTo); + var relRelations = relModel.model.relations; + var joinType = 'LEFT'; var condition = new ParameterizedSQL( 'ON ' + @@ -1194,13 +1197,20 @@ SQLConnector.prototype.buildJoins = function(model, where) { ); if (joinWhere) { + for (var key in joinWhere) { + if (key in relRelations) continue; + + joinType = 'INNER'; + break; + } + var innerWhere = self._buildWhere(modelTo, joinWhere); if (innerWhere.sql.length) { condition.merge(['AND', innerWhere]); } } - return new ParameterizedSQL('INNER JOIN ' + modelToEscaped, []) + return new ParameterizedSQL(joinType + ' JOIN ' + modelToEscaped, []) .merge(condition) .merge(self.buildJoins(modelTo, joinWhere)); }; diff --git a/test/sql.test.js b/test/sql.test.js index e9123f84..7458520f 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -349,6 +349,16 @@ describe('sql connector', function() { }); }); + it('builds LEFT JOIN', function () { + var sql = connector.buildJoins('customer', {orders: {}}); + expect(sql.toJSON()).to.eql({ + sql: + 'LEFT JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ', + params: [] + }); + }); + it('builds SELECT with INNER JOIN (1:n relation)', function () { var sql = connector.buildSelect('customer', { where: { @@ -391,7 +401,7 @@ describe('sql connector', function() { 'SELECT DISTINCT `STORE`.`ID`,' + '`STORE`.`STATE` ' + 'FROM `STORE` ' + - 'INNER JOIN `ORDER` ' + + 'LEFT JOIN `ORDER` ' + 'ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + 'INNER JOIN `CUSTOMER` ' + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + @@ -401,7 +411,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN with or', function () { + it('builds SELECT with JOIN with or', function () { var sql = connector.buildSelect('customer', { where: { or: [{ @@ -423,9 +433,13 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2) ' + - 'OR (`ORDER`.`DATE` BETWEEN $3 AND $4) ' + + 'LEFT JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'WHERE (' + + '`ORDER`.`DATE` BETWEEN $1 AND $2' + + ') OR (' + + '`ORDER`.`DATE` BETWEEN $3 AND $4' + + ') ' + 'ORDER BY `CUSTOMER`.`NAME`', params: [ '2015-01-01', @@ -436,7 +450,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN with and', function () { + it('builds SELECT with JOIN with and', function () { var sql = connector.buildSelect('customer', { where: { and: [{ @@ -458,7 +472,7 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + + 'LEFT JOIN `ORDER` ' + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2) ' + 'AND (`ORDER`.`DATE` BETWEEN $3 AND $4) ' + @@ -472,7 +486,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN with or and nested relation', function () { + it('builds SELECT with JOIN with or and nested relation', function () { var sql = connector.buildSelect('customer', { where: { or: [{ @@ -496,9 +510,9 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + + 'LEFT JOIN `ORDER` ' + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'INNER JOIN `STORE` ' + + 'LEFT JOIN `STORE` ' + 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + 'WHERE (`STORE`.`STATE`=$1) ' + 'OR (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + @@ -511,7 +525,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN with or and nested relation with non relation search', function () { + it('builds SELECT with JOIN with or and nested relation with non relation search', function () { var sql = connector.buildSelect('customer', { where: { or: [{ @@ -539,9 +553,9 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + + 'LEFT JOIN `ORDER` ' + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'INNER JOIN `STORE` ' + + 'LEFT JOIN `STORE` ' + 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2 AND `STORE`.`STATE`=$3) ' + 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5 AND `STORE`.`STATE`=$6) ' + @@ -557,7 +571,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN with nested or', function () { + it('builds SELECT with JOIN with nested or', function () { var sql = connector.buildSelect('customer', { where: { or: [{ @@ -599,8 +613,8 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `STORE` ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + - 'INNER JOIN `ORDER` ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + + 'LEFT JOIN `STORE` ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + 'LEFT JOIN `ORDER` ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + 'WHERE (' + '`STORE`.`STATE`=$1 AND (' + '`ORDER`.`DATE` BETWEEN $2 AND $3' + @@ -627,7 +641,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN and order by relation columns', function () { + it('builds SELECT with JOIN and order by relation columns', function () { var sql = connector.buildSelect('order', { where: { customer: { @@ -647,7 +661,7 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'INNER JOIN `CUSTOMER` ' + + 'LEFT JOIN `CUSTOMER` ' + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + 'ORDER BY `CUSTOMER`.`VIP` DESC,' + '`CUSTOMER`.`NAME` ASC', @@ -655,7 +669,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN and order by relation columns with where but no conditions', function () { + it('builds SELECT with JOIN and order by relation columns with where but no conditions', function () { var sql = connector.buildSelect('order', { where: { customer: {} @@ -670,7 +684,7 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'INNER JOIN `CUSTOMER` ' + + 'LEFT JOIN `CUSTOMER` ' + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + 'ORDER BY `CUSTOMER`.`VIP` DESC,' + '`CUSTOMER`.`NAME` ASC', @@ -678,7 +692,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN and order by relation columns with any where', function () { + 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'] }); @@ -690,7 +704,7 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'INNER JOIN `CUSTOMER` ' + + 'LEFT JOIN `CUSTOMER` ' + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + 'ORDER BY `CUSTOMER`.`VIP` DESC,' + '`CUSTOMER`.`NAME` ASC', @@ -698,7 +712,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with INNER JOIN and order by nested relation columns', function () { + 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'] }); @@ -710,9 +724,9 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'INNER JOIN `CUSTOMER` ' + + 'LEFT JOIN `CUSTOMER` ' + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'INNER JOIN `STORE` ' + + 'LEFT JOIN `STORE` ' + 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + 'ORDER BY `STORE`.`STATE` DESC,' + '`CUSTOMER`.`NAME` ASC', @@ -787,7 +801,7 @@ describe('sql connector', function() { 'INNER JOIN `CUSTOMER` ' + 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + 'AND `CUSTOMER`.`NAME`=$1 ' + - 'INNER JOIN `STORE` ' + + 'LEFT JOIN `STORE` ' + 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + 'WHERE `ORDER`.`DATE` BETWEEN $2 AND $3 ' + 'ORDER BY `STORE`.`STATE` DESC,' + @@ -857,7 +871,7 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,'+ '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + + 'LEFT JOIN `ORDER` ' + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + 'INNER JOIN `STORE` ' + 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` AND `STORE`.`STATE`=$1 ' + @@ -920,11 +934,11 @@ describe('sql connector', function() { sql: 'SELECT count(DISTINCT `CUSTOMER`.`NAME`) as \"cnt\" ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'LEFT JOIN `ORDER` ' + + 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + 'WHERE `CUSTOMER`.`NAME`=$1 ' + - 'AND (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + - 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5)', + 'AND (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + + 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5)', params: [ 'John', '2015-01-01', From c5b60722d9bd5decb78f560841d929575d52eba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mahieu?= Date: Mon, 29 Feb 2016 12:21:07 +0100 Subject: [PATCH 5/6] Use scoped joins for allowing join on model with the same name --- lib/sql.js | 66 ++++++++++++----- test/sql.test.js | 185 ++++++++++++++++++++++++----------------------- 2 files changed, 143 insertions(+), 108 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index d15c7bec..c33b8c10 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -228,8 +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.tableEscaped(model) + '.' + this.escapeName(this.column(model, property)); +SQLConnector.prototype.columnEscaped = function(model, property, relName) { + var prefix = ''; + if (!relName) { + prefix = this.tableEscaped(model); + } else { + prefix = this.escapeName(relName); + } + + return prefix + '.' + this.escapeName(this.column(model, property)); }; /*! @@ -732,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(''); } @@ -764,22 +771,30 @@ SQLConnector.prototype._buildWhere = function(model, where) { if (ck in relations) { // Build a where clause with relation model var relClauses = []; - var relModel = relations[ck].modelTo; + 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) { - relModel = relModel.relations[wk].modelTo; + 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)); + relClauses.push(self._buildWhere(loopRelModel.definition.name, w, loopRelName)); } } } while(lastRelWhere !== relWhere); @@ -819,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; @@ -919,6 +934,7 @@ SQLConnector.prototype.buildOrderBy = function(model, order) { // Column name is in the format: relationName.columnName var colSplit = t[0].split('.'); var prevModel = model; + var relName = ''; for (var y = 0; y < colSplit.length; y++) { if (y >= colSplit.length - 1) { break; @@ -928,8 +944,13 @@ SQLConnector.prototype.buildOrderBy = function(model, order) { 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]); + colName = self.columnEscaped(colModel, colSplit[colSplit.length - 1], relName); } } @@ -1173,13 +1194,14 @@ 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, joinWhere) { + 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 + @@ -1192,8 +1214,8 @@ SQLConnector.prototype.buildJoins = function(model, where) { var condition = new ParameterizedSQL( 'ON ' + - self.columnEscaped(modelFrom, keyFrom) + '=' + - self.columnEscaped(modelTo, keyTo) + self.columnEscaped(modelFrom, keyFrom, forcePrevRelName || prevRelName) + '=' + + self.columnEscaped(modelTo, keyTo, relName) ); if (joinWhere) { @@ -1204,15 +1226,18 @@ SQLConnector.prototype.buildJoins = function(model, where) { break; } - var innerWhere = self._buildWhere(modelTo, joinWhere); + var innerWhere = self._buildWhere(modelTo, joinWhere, relName); if (innerWhere.sql.length) { condition.merge(['AND', innerWhere]); } } - return new ParameterizedSQL(joinType + ' JOIN ' + modelToEscaped, []) + var join = joinType + ' JOIN ' + modelToEscaped; + join += ' AS ' + self.escapeName(relName); + + return new ParameterizedSQL(join, []) .merge(condition) - .merge(self.buildJoins(modelTo, joinWhere)); + .merge(self.buildJoins(modelTo, joinWhere, relName)); }; for (var key in where) { @@ -1222,19 +1247,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].where); + join = buildOneToMany(relName, model, keyFrom, modelTo, keyTo, where[key].where); } else { // n:m relation var modelThrough = rel.modelThrough.definition.name; var keyThrough = rel.keyThrough; var modelToKey = rel.modelTo.definition.idName(); - var joinInner = buildOneToMany(model, keyFrom, modelThrough, keyTo); - join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key].where); + var joinInner = buildOneToMany(relName + '_though', model, keyFrom, modelThrough, keyTo); + join = buildOneToMany(relName, modelThrough, keyThrough, modelTo, modelToKey, where[key].where, + relName + '_though'); join = joinInner.merge(join); } stmt.merge(join); diff --git a/test/sql.test.js b/test/sql.test.js index 7458520f..e75ee5a6 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -343,8 +343,8 @@ describe('sql connector', function() { var sql = connector.buildJoins('customer', {orders: {where: {id: 10}}}); expect(sql.toJSON()).to.eql({ sql: - 'INNER JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` AND `ORDER`.`ID`=? ', + 'INNER JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` AND `orders`.`ID`=? ', params: [10] }); }); @@ -353,8 +353,8 @@ describe('sql connector', function() { var sql = connector.buildJoins('customer', {orders: {}}); expect(sql.toJSON()).to.eql({ sql: - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ', + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ', params: [] }); }); @@ -377,9 +377,9 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'AND `ORDER`.`DATE` BETWEEN $1 AND $2 ' + + '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'] }); @@ -401,11 +401,11 @@ describe('sql connector', function() { 'SELECT DISTINCT `STORE`.`ID`,' + '`STORE`.`STATE` ' + 'FROM `STORE` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + - 'INNER JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'AND `CUSTOMER`.`VIP`=$1 ' + + '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] }); @@ -433,12 +433,12 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + 'WHERE (' + - '`ORDER`.`DATE` BETWEEN $1 AND $2' + + '`orders`.`DATE` BETWEEN $1 AND $2' + ') OR (' + - '`ORDER`.`DATE` BETWEEN $3 AND $4' + + '`orders`.`DATE` BETWEEN $3 AND $4' + ') ' + 'ORDER BY `CUSTOMER`.`NAME`', params: [ @@ -472,10 +472,13 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2) ' + - 'AND (`ORDER`.`DATE` BETWEEN $3 AND $4) ' + + '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', @@ -510,12 +513,12 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'LEFT JOIN `STORE` ' + - 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + - 'WHERE (`STORE`.`STATE`=$1) ' + - 'OR (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + + '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', @@ -553,12 +556,12 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'LEFT JOIN `STORE` ' + - 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + - 'WHERE (`ORDER`.`DATE` BETWEEN $1 AND $2 AND `STORE`.`STATE`=$3) ' + - 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5 AND `STORE`.`STATE`=$6) ' + + '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', @@ -613,17 +616,19 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `STORE` ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + - 'LEFT JOIN `ORDER` ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + + '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 (' + - '`STORE`.`STATE`=$1 AND (' + - '`ORDER`.`DATE` BETWEEN $2 AND $3' + + '`favorite_store`.`STATE`=$1 AND (' + + '`orders`.`DATE` BETWEEN $2 AND $3' + ') ' + - 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5)' + + 'OR (`orders`.`DATE` BETWEEN $4 AND $5)' + ') OR (' + - '`STORE`.`STATE`=$6 ' + - 'AND (`ORDER`.`DATE` BETWEEN $7 AND $8) ' + - 'AND (`ORDER`.`DATE` BETWEEN $9 AND $10)' + + '`favorite_store`.`STATE`=$6 ' + + 'AND (`orders`.`DATE` BETWEEN $7 AND $8) ' + + 'AND (`orders`.`DATE` BETWEEN $9 AND $10)' + ') ' + 'ORDER BY `CUSTOMER`.`NAME`', params: [ @@ -661,10 +666,10 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'LEFT JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'ORDER BY `CUSTOMER`.`VIP` DESC,' + - '`CUSTOMER`.`NAME` ASC', + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'ORDER BY `customer`.`VIP` DESC,' + + '`customer`.`NAME` ASC', params: [] }); }); @@ -684,10 +689,10 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'LEFT JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'ORDER BY `CUSTOMER`.`VIP` DESC,' + - '`CUSTOMER`.`NAME` ASC', + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'ORDER BY `customer`.`VIP` DESC,' + + '`customer`.`NAME` ASC', params: [] }); }); @@ -704,10 +709,10 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'LEFT JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'ORDER BY `CUSTOMER`.`VIP` DESC,' + - '`CUSTOMER`.`NAME` ASC', + 'LEFT JOIN `CUSTOMER` AS `customer` ' + + 'ON `ORDER`.`CUSTOMER_NAME`=`customer`.`NAME` ' + + 'ORDER BY `customer`.`VIP` DESC,' + + '`customer`.`NAME` ASC', params: [] }); }); @@ -724,12 +729,12 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'LEFT JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'LEFT JOIN `STORE` ' + - 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + - 'ORDER BY `STORE`.`STATE` DESC,' + - '`CUSTOMER`.`NAME` ASC', + '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: [] }); }); @@ -760,15 +765,15 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'INNER JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'AND `CUSTOMER`.`NAME`=$1 ' + - 'INNER JOIN `STORE` ' + - 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + - 'AND `STORE`.`STATE`=$2 ' + + '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 `STORE`.`STATE` DESC,' + - '`CUSTOMER`.`NAME` ASC', + 'ORDER BY `customer_favorite_store`.`STATE` DESC,' + + '`customer`.`NAME` ASC', params: [ 'foo', 'NY', @@ -798,14 +803,14 @@ describe('sql connector', function() { '`ORDER`.`CUSTOMER_NAME`,' + '`ORDER`.`STORE_ID` ' + 'FROM `ORDER` ' + - 'INNER JOIN `CUSTOMER` ' + - 'ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ' + - 'AND `CUSTOMER`.`NAME`=$1 ' + - 'LEFT JOIN `STORE` ' + - 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + + '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 `STORE`.`STATE` DESC,' + - '`CUSTOMER`.`NAME` ASC', + 'ORDER BY `customer_favorite_store`.`STATE` DESC,' + + '`customer`.`NAME` ASC', params: [ 'foo', '2015-01-01', @@ -838,12 +843,12 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'AND `ORDER`.`DATE` BETWEEN $1 AND $2 ' + - 'INNER JOIN `STORE` ' + - 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ' + - 'AND `STORE`.`STATE`=$3 ' + + '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'] }); @@ -871,10 +876,10 @@ describe('sql connector', function() { '`CUSTOMER`.`ADDRESS`,'+ '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'INNER JOIN `STORE` ' + - 'ON `ORDER`.`STORE_ID`=`STORE`.`ID` AND `STORE`.`STATE`=$1 ' + + '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'] }); @@ -909,9 +914,9 @@ describe('sql connector', function() { sql: 'SELECT count(DISTINCT `CUSTOMER`.`NAME`) as \"cnt\" ' + 'FROM `CUSTOMER` ' + - 'INNER JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + - 'AND `ORDER`.`DATE` BETWEEN $1 AND $2 ' + + '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'] }); @@ -934,11 +939,11 @@ describe('sql connector', function() { sql: 'SELECT count(DISTINCT `CUSTOMER`.`NAME`) as \"cnt\" ' + 'FROM `CUSTOMER` ' + - 'LEFT JOIN `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ' + + 'LEFT JOIN `ORDER` AS `orders` ' + + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + 'WHERE `CUSTOMER`.`NAME`=$1 ' + - 'AND (`ORDER`.`DATE` BETWEEN $2 AND $3) ' + - 'OR (`ORDER`.`DATE` BETWEEN $4 AND $5)', + 'AND (`orders`.`DATE` BETWEEN $2 AND $3) ' + + 'OR (`orders`.`DATE` BETWEEN $4 AND $5)', params: [ 'John', '2015-01-01', From b93aed6a888cf3fac5137fb4f5aba5510f43c39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mahieu?= Date: Sat, 27 Feb 2016 13:28:05 +0100 Subject: [PATCH 6/6] Refactor query-relations for avoid nested where --- lib/sql.js | 7 +++-- test/sql.test.js | 66 ++++++++++++++---------------------------------- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index c33b8c10..aa0f6f9f 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -1130,8 +1130,7 @@ SQLConnector.prototype._prepareFilter = function (model, filter) { fixWhere(where[key], relations, dest); } else if (key in relations) { dest[key] = dest[key] || {}; - dest[key].where = dest[key].where || {}; - fixWhere(where[key], relations[key].modelTo.relations, dest[key].where); + fixWhere(where[key], relations[key].modelTo.relations, dest[key]); } } } @@ -1255,7 +1254,7 @@ SQLConnector.prototype.buildJoins = function(model, where, prevRelName) { var join; if (!rel.modelThrough) { // 1:n relation - join = buildOneToMany(relName, model, keyFrom, modelTo, keyTo, where[key].where); + join = buildOneToMany(relName, model, keyFrom, modelTo, keyTo, where[key]); } else { // n:m relation var modelThrough = rel.modelThrough.definition.name; @@ -1263,7 +1262,7 @@ SQLConnector.prototype.buildJoins = function(model, where, prevRelName) { var modelToKey = rel.modelTo.definition.idName(); var joinInner = buildOneToMany(relName + '_though', model, keyFrom, modelThrough, keyTo); - join = buildOneToMany(relName, modelThrough, keyThrough, modelTo, modelToKey, where[key].where, + join = buildOneToMany(relName, modelThrough, keyThrough, modelTo, modelToKey, where[key], relName + '_though'); join = joinInner.merge(join); } diff --git a/test/sql.test.js b/test/sql.test.js index e75ee5a6..076ddb79 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -340,7 +340,7 @@ describe('sql connector', function() { }); 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 `ORDER` AS `orders` ' + @@ -363,9 +363,7 @@ 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']} } } }); @@ -389,9 +387,7 @@ describe('sql connector', function() { var sql = connector.buildSelect('store', { where: { customers: { - where: { - vip: true - } + vip: true } } }); @@ -429,9 +425,9 @@ describe('sql connector', function() { expect(sql.toJSON()).to.eql({ sql: 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + - '`CUSTOMER`.`VIP`,' + - '`CUSTOMER`.`ADDRESS`,' + - '`CUSTOMER`.`FAVORITE_STORE` ' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + 'LEFT JOIN `ORDER` AS `orders` ' + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + @@ -468,9 +464,9 @@ describe('sql connector', function() { expect(sql.toJSON()).to.eql({ sql: 'SELECT DISTINCT `CUSTOMER`.`NAME`,' + - '`CUSTOMER`.`VIP`,' + - '`CUSTOMER`.`ADDRESS`,' + - '`CUSTOMER`.`FAVORITE_STORE` ' + + '`CUSTOMER`.`VIP`,' + + '`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`FAVORITE_STORE` ' + 'FROM `CUSTOMER` ' + 'LEFT JOIN `ORDER` AS `orders` ' + 'ON `CUSTOMER`.`NAME`=`orders`.`CUSTOMER_NAME` ' + @@ -648,14 +644,6 @@ describe('sql connector', function() { it('builds SELECT with JOIN and order by relation columns', function () { var sql = connector.buildSelect('order', { - where: { - customer: { - fields: { - 'name': true, - 'vip': true - } - } - }, order: ['customer.vip DESC', 'customer.name ASC'] }); @@ -744,14 +732,10 @@ describe('sql connector', function() { where: { date: {between: ['2015-01-01', '2015-01-31']}, customer: { - where: { - name: 'foo', - /*jshint camelcase:false */ - favorite_store: { - where: { - state: 'NY' - } - } + name: 'foo', + /*jshint camelcase:false */ + favorite_store: { + state: 'NY' } } }, @@ -788,9 +772,7 @@ describe('sql connector', function() { where: { date: {between: ['2015-01-01', '2015-01-31']}, customer: { - where: { - name: 'foo' - } + name: 'foo' } }, order: ['customer.favorite_store.state DESC', 'customer.name ASC'] @@ -823,15 +805,11 @@ 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' } } }); @@ -858,12 +836,8 @@ describe('sql connector', function() { var sql = connector.buildSelect('customer', { where: { orders: { - where: { - store: { - where: { - state: 'NY' - } - } + store: { + state: 'NY' } } } @@ -905,9 +879,7 @@ 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({