From c85c3b91e999c1e5c65d83818ca908c2ea642336 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 22 Mar 2016 22:44:46 -0700 Subject: [PATCH] Tidy up where for relations --- lib/sql.js | 83 +++++++++++++++++----------- test/sql.test.js | 137 ++++++++++++++++++++++++----------------------- 2 files changed, 123 insertions(+), 97 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index 08ecc27..3148e32 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -230,7 +230,23 @@ SQLConnector.prototype.tableEscaped = function(model) { * @returns {String} The escaped column name */ SQLConnector.prototype.columnEscaped = function(model, property) { - return this.tableEscaped(model) + '.' + this.escapeName(this.column(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)); }; /*! @@ -1017,18 +1033,7 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { } } - var haveRelationFilters = false; - if (filter.where) { - var relations = this.getModelDefinition(model).model.relations; - if (relations) { - for (var key in filter.where) { - if (key in relations) { - haveRelationFilters = true; - break; - } - } - } - } + var haveRelationFilters = this.hasRelationClause(model, filter.where); var distinct = haveRelationFilters ? 'DISTINCT ' : ''; var selectStmt = new ParameterizedSQL('SELECT ' + distinct + @@ -1077,17 +1082,22 @@ SQLConnector.prototype.buildJoins = function(model, where) { var relations = modelDef.model.relations; var stmt = new ParameterizedSQL('', []); + var self = this; var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) { - var modelToEscaped = this.tableEscaped(modelTo); + 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 = this.columnEscaped(modelFrom, keyFrom) + '=' + - this.columnEscaped(modelTo, keyTo); + var condition = self.qualifiedColumnEscaped(modelFrom, keyFrom) + '=' + + self.qualifiedColumnEscaped(modelTo, keyTo); - var innerSelect = this.buildSelect(modelTo, innerFilter, { + var innerSelect = self.buildSelect(modelTo, innerFilter, { skipParameterize: true }); @@ -1095,7 +1105,7 @@ SQLConnector.prototype.buildJoins = function(model, where) { .merge(innerSelect) .merge(') AS ' + modelToEscaped) .merge('ON ' + condition); - }.bind(this); + }; for (var key in where) { if (!(key in relations)) continue; @@ -1113,7 +1123,7 @@ SQLConnector.prototype.buildJoins = function(model, where) { // n:m relation var modelThrough = rel.modelThrough.definition.name; var keyThrough = rel.keyThrough; - var modelToKey = rel.modelTo.definition._ids[0].name; + var modelToKey = rel.modelTo.definition.idName(); var innerFilter = {fields: {}}; innerFilter.fields[keyThrough] = true; @@ -1228,6 +1238,28 @@ SQLConnector.prototype.find = function(model, id, options, cb) { // Alias to `find`. Juggler checks `findById` only. Connector.defineAliases(SQLConnector.prototype, 'find', ['findById']); +/** + * Check if the where statement contains relations + * @param model + * @param where + * @returns {boolean} + */ +SQLConnector.prototype.hasRelationClause = function(model, where) { + var found = false; + if (where) { + var relations = this.getModelDefinition(model).model.relations; + if (relations) { + for (var key in where) { + if (key in relations) { + found = true; + break; + } + } + } + } + return found; +}; + /** * Build a SQL SELECT statement to count rows * @param {String} model Model name @@ -1236,18 +1268,7 @@ Connector.defineAliases(SQLConnector.prototype, 'find', ['findById']); * @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]} */ SQLConnector.prototype.buildCount = function(model, where, options) { - var haveRelationFilters = false; - if (where) { - var relations = this.getModelDefinition(model).model.relations; - if (relations) { - for (var key in where) { - if (key in relations) { - haveRelationFilters = true; - break; - } - } - } - } + var haveRelationFilters = this.hasRelationClause(model, where); var count = 'count(*)'; if (haveRelationFilters) { diff --git a/test/sql.test.js b/test/sql.test.js index a7268b2..cfc2ca3 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -108,6 +108,11 @@ 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`'); }); @@ -119,7 +124,7 @@ describe('sql connector', function() { it('builds where', function() { var where = connector.buildWhere('customer', {name: 'John'}); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME`=?', + sql: 'WHERE `NAME`=?', params: ['John'] }); }); @@ -127,7 +132,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 `CUSTOMER`.`NAME` IS NULL', + sql: 'WHERE `NAME` IS NULL', params: [] }); }); @@ -135,7 +140,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 `CUSTOMER`.`NAME` IN (?,?)', + sql: 'WHERE `NAME` IN (?,?)', params: ['John', 'Mary'] }); }); @@ -144,7 +149,7 @@ describe('sql connector', function() { var where = connector.buildWhere('customer', {or: [{name: 'John'}, {name: 'Mary'}]}); expect(where.toJSON()).to.eql({ - sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`CUSTOMER`.`NAME`=?)', + sql: 'WHERE (`NAME`=?) OR (`NAME`=?)', params: ['John', 'Mary'] }); }); @@ -153,7 +158,7 @@ describe('sql connector', function() { var where = connector.buildWhere('customer', {and: [{name: 'John'}, {vip: true}]}); expect(where.toJSON()).to.eql({ - sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`CUSTOMER`.`VIP`=?)', + sql: 'WHERE (`NAME`=?) AND (`VIP`=?)', params: ['John', true] }); }); @@ -165,7 +170,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + sql: 'WHERE `NAME` REGEXP ?', params: ['^J'] }); }); @@ -177,7 +182,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + sql: 'WHERE `NAME` REGEXP ?', params: ['^J/i'] }); }); @@ -189,7 +194,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + sql: 'WHERE `NAME` REGEXP ?', params: [/^J/] }); }); @@ -201,7 +206,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + sql: 'WHERE `NAME` REGEXP ?', params: [/^J/i] }); }); @@ -213,7 +218,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + sql: 'WHERE `NAME` REGEXP ?', params: [/^J/] }); }); @@ -225,7 +230,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + sql: 'WHERE `NAME` REGEXP ?', params: [new RegExp(/^J/i)] }); }); @@ -234,31 +239,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 (`CUSTOMER`.`NAME`=?) AND ((`CUSTOMER`.`VIP`=?) OR ' + - '(`CUSTOMER`.`ADDRESS` IS NULL))', + sql: 'WHERE (`NAME`=?) AND ((`VIP`=?) OR ' + + '(`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 `CUSTOMER`.`NAME`'); + expect(orderBy).to.eql('ORDER BY `NAME`'); }); it('builds order by with two fields', function() { var orderBy = connector.buildOrderBy('customer', ['name', 'vip']); - expect(orderBy).to.eql('ORDER BY `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`'); + expect(orderBy).to.eql('ORDER BY `NAME`,`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 `CUSTOMER`.`NAME` ASC,`CUSTOMER`.`VIP` DESC'); + expect(orderBy).to.eql('ORDER BY `NAME` ASC,`VIP` DESC'); }); it('builds fields for columns', function() { var fields = connector.buildFields('customer', {name: 'John', vip: true, unknown: 'Random'}); - expect(fields.names).to.eql(['`CUSTOMER`.`NAME`', '`CUSTOMER`.`VIP`']); + expect(fields.names).to.eql(['`NAME`', '`VIP`']); expect(fields.columnValues[0].toJSON()).to.eql( {sql: '?', params: ['John']}); expect(fields.columnValues[1].toJSON()).to.eql( @@ -269,7 +274,7 @@ describe('sql connector', function() { var fields = connector.buildFieldsForUpdate('customer', {name: 'John', vip: true}); expect(fields.toJSON()).to.eql({ - sql: 'SET `CUSTOMER`.`VIP`=?', + sql: 'SET `VIP`=?', params: [true] }); }); @@ -278,36 +283,36 @@ describe('sql connector', function() { var fields = connector.buildFieldsForUpdate('customer', {name: 'John', vip: true}, false); expect(fields.toJSON()).to.eql({ - sql: 'SET `CUSTOMER`.`NAME`=?,`CUSTOMER`.`VIP`=?', + sql: 'SET `NAME`=?,`VIP`=?', params: ['John', true] }); }); it('builds column names for SELECT', function() { var cols = connector.buildColumnNames('customer'); - expect(cols).to.eql('`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,' + - '`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE`'); + expect(cols).to.eql('`NAME`,`VIP`,' + + '`ADDRESS`,`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('`CUSTOMER`.`NAME`'); + expect(cols).to.eql('`NAME`'); }); it('builds column names with false fields filter for SELECT', function() { var cols = connector.buildColumnNames('customer', {fields: {name: false}}); - expect(cols).to.eql('`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE`'); + expect(cols).to.eql('`VIP`,`ADDRESS`,`FAVORITE_STORE`'); }); it('builds column names with array fields filter for SELECT', function() { var cols = connector.buildColumnNames('customer', {fields: ['name']}); - expect(cols).to.eql('`CUSTOMER`.`NAME`'); + expect(cols).to.eql('`NAME`'); }); it('builds DELETE', function() { var sql = connector.buildDelete('customer', {name: 'John'}); expect(sql.toJSON()).to.eql({ - sql: 'DELETE FROM `CUSTOMER` WHERE `CUSTOMER`.`NAME`=$1', + sql: 'DELETE FROM `CUSTOMER` WHERE `NAME`=$1', params: ['John'] }); }); @@ -315,7 +320,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 `CUSTOMER`.`VIP`=$1 WHERE `CUSTOMER`.`NAME`=$2', + sql: 'UPDATE `CUSTOMER` SET `VIP`=$1 WHERE `NAME`=$2', params: [false, 'John'] }); }); @@ -324,9 +329,9 @@ describe('sql connector', function() { var sql = connector.buildSelect('customer', {order: 'name', limit: 5, where: {name: 'John'}}); expect(sql.toJSON()).to.eql({ - sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + - '`CUSTOMER`.`FAVORITE_STORE` FROM `CUSTOMER` WHERE `CUSTOMER`.`NAME`=$1 ' + - 'ORDER BY `CUSTOMER`.`NAME` LIMIT 5', + sql: 'SELECT `NAME`,`VIP`,`ADDRESS`,' + + '`FAVORITE_STORE` FROM `CUSTOMER` WHERE `NAME`=$1 ' + + 'ORDER BY `NAME` LIMIT 5', params: ['John'] }); }); @@ -334,7 +339,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`(`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`) VALUES($1,$2)', + sql: 'INSERT INTO `CUSTOMER`(`NAME`,`VIP`) VALUES($1,$2)', params: ['John', true] }); }); @@ -342,8 +347,8 @@ 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 `ORDER`.`CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`ORDER`.`ID`=? ORDER BY `ORDER`.`ID` ) AS `ORDER` ON ' + + sql: 'INNER JOIN ( SELECT `CUSTOMER_NAME` FROM `ORDER` WHERE ' + + '`ID`=? ORDER BY `ID` ) AS `ORDER` ON ' + '`CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME`', params: [10] }); @@ -361,11 +366,11 @@ describe('sql connector', function() { }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,' + - '`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE` FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT `ORDER`.`CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`ORDER`.`DATE` BETWEEN $1 AND $2 ORDER BY `ORDER`.`ID` ) AS `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` ORDER BY `CUSTOMER`.`NAME`', + 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`', params: ['2015-01-01', '2015-01-31'] }); }); @@ -382,12 +387,12 @@ describe('sql connector', function() { }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `STORE`.`ID`,`STORE`.`STATE` FROM `STORE` INNER JOIN' + - ' ( SELECT `ORDER`.`CUSTOMER_NAME`,`ORDER`.`STORE_ID` FROM `ORDER` ' + - 'ORDER BY `ORDER`.`ID` ) AS `ORDER` ON `STORE`.`ID`=`ORDER`.`STORE_ID` ' + - 'INNER JOIN ( SELECT `CUSTOMER`.`NAME` FROM `CUSTOMER` WHERE ' + - '`CUSTOMER`.`VIP`=$1 ORDER BY `CUSTOMER`.`NAME` ) AS `CUSTOMER` ON ' + - '`ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ORDER BY `STORE`.`ID`', + 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`', params: [true] }); }); @@ -406,11 +411,11 @@ describe('sql connector', function() { }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `ORDER`.`ID`,`ORDER`.`DATE`,`ORDER`.`CUSTOMER_NAME`,' + - '`ORDER`.`STORE_ID` FROM `ORDER` INNER JOIN ( SELECT `CUSTOMER`.`NAME`,' + - '`CUSTOMER`.`VIP` FROM `CUSTOMER` ORDER BY `CUSTOMER`.`NAME` ) AS `CUSTOMER`' + + 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 ' + - '`CUSTOMER`.`VIP` DESC,`CUSTOMER`.`NAME` ASC', + '`VIP` DESC,`NAME` ASC', params: [] }); }); @@ -433,13 +438,13 @@ describe('sql connector', function() { }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,' + - '`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE` FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT `ORDER`.`CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`ORDER`.`DATE` BETWEEN $1 AND $2 ORDER BY `ORDER`.`ID` ) AS `ORDER` ON ' + - '`CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` INNER JOIN ( SELECT `STORE`.`ID` ' + - 'FROM `STORE` WHERE `STORE`.`STATE`=$3 ORDER BY `STORE`.`ID` ) AS `STORE` ' + - 'ON `CUSTOMER`.`FAVORITE_STORE`=`STORE`.`ID` ORDER BY `CUSTOMER`.`NAME`', + 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`', params: ['2015-01-01', '2015-01-31', 'NY'] }); }); @@ -460,13 +465,13 @@ describe('sql connector', function() { }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT DISTINCT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,' + - '`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`FAVORITE_STORE` FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT DISTINCT `ORDER`.`CUSTOMER_NAME` FROM `ORDER` ' + - 'INNER JOIN ( SELECT `STORE`.`ID` FROM `STORE` WHERE `STORE`.`STATE`=$1 ' + - 'ORDER BY `STORE`.`ID` ) AS `STORE` ON `ORDER`.`STORE_ID`=`STORE`.`ID` ' + - 'ORDER BY `ORDER`.`ID` ) AS `ORDER` ON `CUSTOMER`.`NAME`=`ORDER`.' + - '`CUSTOMER_NAME` ORDER BY `CUSTOMER`.`NAME`', + 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`', params: ['NY'] }); }); @@ -482,7 +487,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 `CUSTOMER`.`NAME`=$1', + sql: 'SELECT count(*) as "cnt" FROM `CUSTOMER` WHERE `NAME`=$1', params: ['John'] }); }); @@ -497,10 +502,10 @@ describe('sql connector', function() { } }); expect(sql.toJSON()).to.eql({ - sql: 'SELECT count(DISTINCT `CUSTOMER`.`NAME`) as "cnt" FROM `CUSTOMER` ' + - 'INNER JOIN ( SELECT `ORDER`.`CUSTOMER_NAME` FROM `ORDER` WHERE ' + - '`ORDER`.`DATE` BETWEEN $1 AND $2 ORDER BY `ORDER`.`ID` ) AS `ORDER` ' + - 'ON `CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME` WHERE `CUSTOMER`.`NAME`=$3', + 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', params: ['2015-01-01', '2015-01-31', 'John'] }); });