From bab65302d0aec6aa3ea052828f8227de2a4a2c4c Mon Sep 17 00:00:00 2001 From: kolach Date: Thu, 17 Mar 2016 11:22:47 -0600 Subject: [PATCH 01/12] diogo patch to support relation queries --- lib/sql.js | 167 ++++++++++++++++++++++++++++--- lib/utils.js | 18 ++++ test/sql.test.js | 255 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 399 insertions(+), 41 deletions(-) create mode 100644 lib/utils.js diff --git a/lib/sql.js b/lib/sql.js index d9848ecb..e7c1c063 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -5,6 +5,7 @@ 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,7 +230,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)); + return this.tableEscaped(model) + '.' + this.escapeName(this.column(model, property)); }; /*! @@ -406,10 +407,6 @@ SQLConnector.prototype.execute = function(sql, params, options, callback) { }; this.notifyObserversAround('execute', context, function(context, done) { self.executeSQL(context.req.sql, context.req.params, context.options, function(err, info) { - if(err){ - debug('Error: %j %j %j', err, context.req.sql, context.req.params); - } - if (!err && info != null) { context.res = info; } @@ -741,11 +738,17 @@ SQLConnector.prototype._buildWhere = function(model, where) { return new ParameterizedSQL(''); } var self = this; - var props = self.getModelDefinition(model).properties; + var modelDef = self.getModelDefinition(model); + var props = modelDef.properties; + var relations = modelDef.model.relations; var whereStmts = []; for (var key in where) { var stmt = new ParameterizedSQL('', []); + if (relations && key in relations) { + // relationships are handled on joins + continue; + } // Handle and/or operators if (key === 'and' || key === 'or') { var branches = []; @@ -860,10 +863,24 @@ SQLConnector.prototype.buildOrderBy = function(model, order) { var clauses = []; for (var i = 0, n = order.length; i < n; i++) { var t = order[i].split(/[\s,]+/); + var colName; + if (t[0].indexOf('.') < 0) { + colName = self.columnEscaped(model, t[0]); + } 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]); + } + if (t.length === 1) { - clauses.push(self.columnEscaped(model, order[i])); + clauses.push(colName); } else { - clauses.push(self.columnEscaped(model, t[0]) + ' ' + t[1]); + clauses.push(colName + ' ' + t[1]); } } return 'ORDER BY ' + clauses.join(','); @@ -987,6 +1004,8 @@ SQLConnector.prototype.buildColumnNames = function(model, filter) { * @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]} */ SQLConnector.prototype.buildSelect = function(model, filter, options) { + options = options || {}; + if (!filter.order) { var idNames = this.idNames(model); if (idNames && idNames.length) { @@ -994,11 +1013,30 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { } } - var selectStmt = new ParameterizedSQL('SELECT ' + + 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 distinct = haveRelationFilters ? 'DISTINCT ' : ''; + + var selectStmt = new ParameterizedSQL('SELECT ' + distinct + this.buildColumnNames(model, filter) + ' FROM ' + this.tableEscaped(model) ); + if (haveRelationFilters) { + var joinsStmts = this.buildJoins(model, filter.where); + selectStmt.merge(joinsStmts); + } + if (filter) { if (filter.where) { @@ -1016,9 +1054,75 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { } } + + if (options.skipParameterize === true) { + return selectStmt; + } + return this.parameterize(selectStmt); }; +/** + * Build the SQL INNER JOIN clauses + * @param {string} model Model name + * @param {object} where An object for the where conditions + * @returns {ParameterizedSQL} The SQL INNER JOIN clauses + */ +SQLConnector.prototype.buildJoins = function(model, where) { + var modelDef = this.getModelDefinition(model); + var relations = modelDef.model.relations; + var stmt = new ParameterizedSQL('', []); + + var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) { + var modelToEscaped = this.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 innerSelect = this.buildSelect(modelTo, innerFilter, { + skipParameterize: true + }); + + return new ParameterizedSQL('INNER JOIN (', []) + .merge(innerSelect) + .merge(') AS ' + modelToEscaped) + .merge('ON ' + condition); + }.bind(this); + + for (var key in where) { + if (!(key in relations)) continue; + + var rel = relations[key]; + var keyFrom = rel.keyFrom; + var modelTo = rel.modelTo.definition.name; + var keyTo = rel.keyTo; + + var join; + if (!rel.modelThrough) { + // 1:n relation + join = buildOneToMany(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._ids[0].name; + var innerFilter = {fields: {}}; + innerFilter.fields[keyThrough] = true; + + var joinInner = buildOneToMany(model, keyFrom, modelThrough, keyTo, innerFilter); + join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key]); + join = joinInner.merge(join); + } + stmt.merge(join); + } + + return stmt; +}; + /** * Transform the row data into a model data object * @param {string} model Model name @@ -1120,6 +1224,45 @@ SQLConnector.prototype.find = function(model, id, options, cb) { // Alias to `find`. Juggler checks `findById` only. Connector.defineAliases(SQLConnector.prototype, 'find', ['findById']); +/** + * Build a SQL SELECT statement to count rows + * @param {String} model Model name + * @param {Object} where Where object + * @param {Object} options Options object + * @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 count = 'count(*)'; + if (haveRelationFilters) { + var idColumn = this.columnEscaped(model, this.idColumn(model)); + count = 'count(DISTINCT ' + idColumn + ')'; + } + + var stmt = new ParameterizedSQL('SELECT ' + count + + ' as "cnt" FROM ' + this.tableEscaped(model)); + + if (haveRelationFilters) { + var joinsStmts = this.buildJoins(model, where); + stmt = stmt.merge(joinsStmts); + } + + stmt = stmt.merge(this.buildWhere(model, where)); + return this.parameterize(stmt); +}; + /** * Count all model instances by the where filter * @@ -1137,10 +1280,8 @@ SQLConnector.prototype.count = function(model, where, options, cb) { where = tmp; } - var stmt = new ParameterizedSQL('SELECT count(*) as "cnt" FROM ' + - this.tableEscaped(model)); - stmt = stmt.merge(this.buildWhere(model, where)); - stmt = this.parameterize(stmt); + var stmt = this.buildCount(model, where, options); + this.execute(stmt.sql, stmt.params, function(err, res) { if (err) { diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..37e0e577 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,18 @@ +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 dd89e1e7..a7268b29 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -10,6 +10,8 @@ var ds = new juggler.DataSource({ }); var connector; var Customer; +var Order; +var Store; describe('sql connector', function() { before(function() { @@ -35,6 +37,34 @@ describe('sql connector', function() { address: String }, {testdb: {table: 'CUSTOMER'}}); + Order = ds.createModel('order', + { + id: { + id: true + }, + date: Date + }); + Store = ds.createModel('store', + { + id: { + id: true, + type: String + }, + state: String + }); + // Relations + Customer.hasMany(Order, {as: 'orders', foreignKey: 'customer_name'}); + Order.belongsTo(Customer, {as: 'customer', foreignKey: 'customer_name'}); + Order.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); + Store.hasMany(Order, {as: 'orders', foreignKey: 'store_id'}); + Store.hasMany(Customer, { + as: 'customers', + through: Order, + foreignKey: 'store_id', + keyThrough: 'customer_name' + }); + Customer.belongsTo(Store, {as: 'favorite_store', foreignKey: 'favorite_store'}); + Store.hasMany(Customer, {as: 'customers_fav', foreignKey: 'favorite_store'}); }); it('should map table name', function() { @@ -78,7 +108,7 @@ describe('sql connector', function() { it('should find escaped column name', function() { var column = connector.columnEscaped('customer', 'vip'); - expect(column).to.eql('`VIP`'); + expect(column).to.eql('`CUSTOMER`.`VIP`'); }); it('should convert to escaped id column value', function() { @@ -89,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'] }); }); @@ -97,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: [] }); }); @@ -105,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'] }); }); @@ -114,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'] }); }); @@ -123,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] }); }); @@ -135,7 +165,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: ['^J'] }); }); @@ -147,7 +177,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: ['^J/i'] }); }); @@ -159,7 +189,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [/^J/] }); }); @@ -171,7 +201,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [/^J/i] }); }); @@ -183,7 +213,7 @@ describe('sql connector', function() { } }); expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', params: [/^J/] }); }); @@ -195,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)] }); }); @@ -204,30 +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( @@ -238,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] }); }); @@ -247,35 +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`'); + 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`'); + 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'] }); }); @@ -283,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'] }); }); @@ -292,8 +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` 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'] }); }); @@ -301,11 +334,177 @@ 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}}}); + expect(sql.toJSON()).to.eql({ + sql: 'INNER JOIN ( SELECT `ORDER`.`CUSTOMER_NAME` FROM `ORDER` WHERE ' + + '`ORDER`.`ID`=? ORDER BY `ORDER`.`ID` ) AS `ORDER` ON ' + + '`CUSTOMER`.`NAME`=`ORDER`.`CUSTOMER_NAME`', + params: [10] + }); + }); + + 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']} + } + } + } + }); + + 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`', + params: ['2015-01-01', '2015-01-31'] + }); + }); + + it('builds SELECT with INNER JOIN (n:n relation)', function () { + var sql = connector.buildSelect('store', { + where: { + customers: { + where: { + vip: true + } + } + } + }); + + 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`', + params: [true] + }); + }); + + it('builds SELECT with INNER 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'] + }); + + 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`' + + ' ON `ORDER`.`CUSTOMER_NAME`=`CUSTOMER`.`NAME` ORDER BY ' + + '`CUSTOMER`.`VIP` DESC,`CUSTOMER`.`NAME` ASC', + params: [] + }); + }); + + it('builds SELECT with multiple INNER JOIN', function () { + var sql = connector.buildSelect('customer', { + where: { + orders: { + where: { + date: {between: ['2015-01-01', '2015-01-31']} + } + }, + /*jshint camelcase:false */ + favorite_store: { + where: { + state: 'NY' + } + } + } + }); + + 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`', + params: ['2015-01-01', '2015-01-31', 'NY'] + }); + }); + + it('builds nested SELECTs', function () { + var sql = connector.buildSelect('customer', { + where: { + orders: { + where: { + store: { + where: { + state: 'NY' + } + } + } + } + } + }); + + 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`', + params: ['NY'] + }); + }); + + it('builds count', function() { + var sql = connector.buildCount('customer'); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT count(*) as "cnt" FROM `CUSTOMER` ', + params: [] + }); + }); + + 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', + params: ['John'] + }); + }); + + it('builds count with WHERE and JOIN', function() { + var sql = connector.buildCount('customer', { + name: 'John', + orders: { + where: { + date: {between: ['2015-01-01', '2015-01-31']} + } + } + }); + 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', + params: ['2015-01-01', '2015-01-31', 'John'] + }); + }); + it('normalizes a SQL statement from string', function() { var sql = 'SELECT * FROM `CUSTOMER`'; var stmt = new ParameterizedSQL(sql); From c7a69a8d6161a07959a71807802c0e2cc0c0f186 Mon Sep 17 00:00:00 2001 From: kolach Date: Thu, 17 Mar 2016 11:30:26 -0600 Subject: [PATCH 02/12] bumped version 2.3.1 Relation queries --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e60738b6..2cc8899a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-connector", - "version": "2.3.0", + "version": "2.3.1", "description": "Building blocks for LoopBack connectors", "keywords": [ "StrongLoop", From bae830ded79d485a3b7ea722eab2c5c8be6af19d Mon Sep 17 00:00:00 2001 From: kolach Date: Sat, 19 Mar 2016 21:46:02 -0600 Subject: [PATCH 03/12] inner join alias name set to table name without possible shema prefix --- lib/sql.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index e7c1c063..8f1fded1 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -1074,14 +1074,14 @@ SQLConnector.prototype.buildJoins = function(model, where) { var stmt = new ParameterizedSQL('', []); var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) { - var modelToEscaped = this.tableEscaped(modelTo); + var modelToAlias = this.escapeName(this.table(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); + modelToAlias + '.' + this.escapeName(this.column(modelTo, keyTo)); var innerSelect = this.buildSelect(modelTo, innerFilter, { skipParameterize: true @@ -1089,7 +1089,7 @@ SQLConnector.prototype.buildJoins = function(model, where) { return new ParameterizedSQL('INNER JOIN (', []) .merge(innerSelect) - .merge(') AS ' + modelToEscaped) + .merge(') AS ' + modelToAlias) .merge('ON ' + condition); }.bind(this); From 0258cce9ce07bc4aeecc17c93e19c2d9a6fa3861 Mon Sep 17 00:00:00 2001 From: kolach Date: Tue, 22 Mar 2016 12:40:39 -0600 Subject: [PATCH 04/12] where clause extended to support relation queries --- lib/sql.js | 62 +-- test/sql.another.test.js | 489 +++++++++++++++++++++++ test/sql.test.js | 822 +++++++++++++++++++-------------------- 3 files changed, 940 insertions(+), 433 deletions(-) create mode 100644 test/sql.another.test.js diff --git a/lib/sql.js b/lib/sql.js index d9848ecb..e51f8c2d 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -226,10 +226,16 @@ SQLConnector.prototype.tableEscaped = function(model) { * Get the escaped column name for a given model property * @param {String} model The model name * @param {String} property The property name + * @param {Boolean} qualified Whether qualify column or not * @returns {String} The escaped column name */ -SQLConnector.prototype.columnEscaped = function(model, property) { - return this.escapeName(this.column(model, property)); +SQLConnector.prototype.columnEscaped = function(model, property, qualified) { + var escapedColumn = this.escapeName(this.column(model, property)); + if (qualified) { + return this.tableEscaped(model) + '.' + escapedColumn; + } else { + return escapedColumn; + } }; /*! @@ -460,7 +466,7 @@ SQLConnector.prototype.save = function(model, data, options, cb) { var updateStmt = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model)); updateStmt.merge(this.buildFieldsForUpdate(model, data)); - var whereStmt = this.buildWhere(model, where); + var whereStmt = this.buildWhere(model, where, true); updateStmt.merge(whereStmt); updateStmt = this.parameterize(updateStmt); this.execute(updateStmt.sql, updateStmt.params, options, @@ -487,7 +493,7 @@ SQLConnector.prototype.exists = function(model, id, options, cb) { 'SELECT 1 FROM ' + this.tableEscaped(model) + ' WHERE ' + this.idColumnEscaped(model) ); - selectStmt.merge(this.buildWhere(model, where)); + selectStmt.merge(this.buildWhere(model, where, true)); selectStmt = this.applyPagination(model, selectStmt, { limit: 1, offset: 0, @@ -541,7 +547,7 @@ Connector.defineAliases(SQLConnector.prototype, 'destroy', SQLConnector.prototype.buildDelete = function(model, where, options) { var deleteStmt = new ParameterizedSQL('DELETE FROM ' + this.tableEscaped(model)); - deleteStmt.merge(this.buildWhere(model, where)); + deleteStmt.merge(this.buildWhere(model, where, true)); return this.parameterize(deleteStmt); }; @@ -604,7 +610,7 @@ SQLConnector.prototype.updateAttributes = function(model, id, data, options, cb) SQLConnector.prototype.buildUpdate = function(model, where, data, options) { var fields = this.buildFieldsForUpdate(model, data); var updateClause = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model)); - var whereClause = this.buildWhere(model, where); + var whereClause = this.buildWhere(model, where, true); updateClause.merge([fields, whereClause]); return this.parameterize(updateClause); }; @@ -636,10 +642,11 @@ Connector.defineAliases(SQLConnector.prototype, 'update', ['updateAll']); * Build the SQL WHERE clause for the where object * @param {string} model Model name * @param {object} where An object for the where conditions + * @param {qualified} Qualify column names or not, default false * @returns {ParameterizedSQL} The SQL WHERE clause */ -SQLConnector.prototype.buildWhere = function(model, where) { - var whereClause = this._buildWhere(model, where); +SQLConnector.prototype.buildWhere = function(model, where, qualified) { + var whereClause = this._buildWhere(model, where, qualified); if (whereClause.sql) { whereClause.sql = 'WHERE ' + whereClause.sql; } @@ -729,10 +736,11 @@ SQLConnector.prototype.buildExpression = function(columnName, operator, columnVa /*! * @param model * @param where + * @param qualified * @returns {ParameterizedSQL} * @private */ -SQLConnector.prototype._buildWhere = function(model, where) { +SQLConnector.prototype._buildWhere = function(model, where, qualified) { if (!where) { return new ParameterizedSQL(''); } @@ -741,11 +749,19 @@ SQLConnector.prototype._buildWhere = function(model, where) { return new ParameterizedSQL(''); } var self = this; - var props = self.getModelDefinition(model).properties; + var modelDef = self.getModelDefinition(model); + var props = modelDef.properties; + var relations = modelDef.model.relations; var whereStmts = []; for (var key in where) { var stmt = new ParameterizedSQL('', []); + // Handle nested relation + if (relations && key in relations) { + var stmtForRelation = self._buildWhere(key, where[key], qualified); + whereStmts.push(stmtForRelation); + continue; + } // Handle and/or operators if (key === 'and' || key === 'or') { var branches = []; @@ -753,7 +769,7 @@ 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 stmtForClause = self._buildWhere(model, clauses[i]); + var stmtForClause = self._buildWhere(model, clauses[i], qualified); stmtForClause.sql = '(' + stmtForClause.sql + ')'; branchParams = branchParams.concat(stmtForClause.params); branches.push(stmtForClause.sql); @@ -767,7 +783,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, qualified); var expression = where[key]; var columnValue; var sqlExp; @@ -861,9 +877,9 @@ SQLConnector.prototype.buildOrderBy = function(model, order) { for (var i = 0, n = order.length; i < n; i++) { var t = order[i].split(/[\s,]+/); if (t.length === 1) { - clauses.push(self.columnEscaped(model, order[i])); + clauses.push(self.columnEscaped(model, order[i], true)); } else { - clauses.push(self.columnEscaped(model, t[0]) + ' ' + t[1]); + clauses.push(self.columnEscaped(model, t[0], true) + ' ' + t[1]); } } return 'ORDER BY ' + clauses.join(','); @@ -874,9 +890,10 @@ SQLConnector.prototype.buildOrderBy = function(model, order) { * @param {String} model Model name * @param {Object} data Model data object * @param {Boolean} excludeIds Exclude id properties or not, default to false + * @param {Boolean} qualified Exclude build field names qualified, default false * @returns {{names: Array, values: Array, properties: Array}} */ -SQLConnector.prototype.buildFields = function(model, data, excludeIds) { +SQLConnector.prototype.buildFields = function(model, data, excludeIds, qualified) { var fields = { names: [], // field names columnValues: [], // an array of ParameterizedSQL @@ -896,7 +913,7 @@ SQLConnector.prototype.buildFields = function(model, data, excludeIds) { continue; } - var k = this.columnEscaped(model, key); + var k = this.columnEscaped(model, key, qualified); var v = this.toColumnValue(p, data[key]); if (v !== undefined) { fields.names.push(k); @@ -922,7 +939,7 @@ SQLConnector.prototype.buildFieldsForUpdate = function(model, data, excludeIds) if (excludeIds === undefined) { excludeIds = true; } - var fields = this.buildFields(model, data, excludeIds); + var fields = this.buildFields(model, data, excludeIds, true); var columns = new ParameterizedSQL(''); for (var i = 0, n = fields.names.length; i < n; i++) { var clause = ParameterizedSQL.append(fields.names[i], @@ -937,9 +954,10 @@ SQLConnector.prototype.buildFieldsForUpdate = function(model, data, excludeIds) * Build a list of escaped column names for the given model and fields filter * @param {string} model Model name * @param {object} filter The filter object + * @param {Boolean} qualified Qualify column names or not, default false * @returns {string} Comma separated string of escaped column names */ -SQLConnector.prototype.buildColumnNames = function(model, filter) { +SQLConnector.prototype.buildColumnNames = function(model, filter, qualified) { var fieldsFilter = filter && filter.fields; var cols = this.getModelDefinition(model).properties; if (!cols) { @@ -974,7 +992,7 @@ SQLConnector.prototype.buildColumnNames = function(model, filter) { } } var names = keys.map(function(c) { - return self.columnEscaped(model, c); + return self.columnEscaped(model, c, qualified); }); return names.join(','); }; @@ -995,14 +1013,14 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { } var selectStmt = new ParameterizedSQL('SELECT ' + - this.buildColumnNames(model, filter) + + this.buildColumnNames(model, filter, true) + ' FROM ' + this.tableEscaped(model) ); if (filter) { if (filter.where) { - var whereStmt = this.buildWhere(model, filter.where); + var whereStmt = this.buildWhere(model, filter.where, true); selectStmt.merge(whereStmt); } @@ -1139,7 +1157,7 @@ SQLConnector.prototype.count = function(model, where, options, cb) { var stmt = new ParameterizedSQL('SELECT count(*) as "cnt" FROM ' + this.tableEscaped(model)); - stmt = stmt.merge(this.buildWhere(model, where)); + stmt = stmt.merge(this.buildWhere(model, where, true)); stmt = this.parameterize(stmt); this.execute(stmt.sql, stmt.params, function(err, res) { diff --git a/test/sql.another.test.js b/test/sql.another.test.js new file mode 100644 index 00000000..1647352f --- /dev/null +++ b/test/sql.another.test.js @@ -0,0 +1,489 @@ +var expect = require('chai').expect; +var SQLConnector = require('../lib/sql'); +var ParameterizedSQL = SQLConnector.ParameterizedSQL; +var testConnector = require('./connectors/test-sql-connector'); + +var juggler = require('loopback-datasource-juggler'); +var ds = new juggler.DataSource({ + connector: testConnector, + debug: true +}); +var connector; +var Customer; +var Order; +var Store; + +describe('sql connector', function() { + before(function() { + connector = ds.connector; + connector._tables = {}; + connector._models = {}; + Customer = ds.createModel('customer', + { + name: { + id: true, + type: String, + testdb: { + column: 'NAME', + dataType: 'VARCHAR', + dataLength: 32 + } + }, + vip: { + type: Boolean, + testdb: { + column: 'VIP' + } + }, + address: String + }, + {testdb: {table: 'CUSTOMER'}} + ); + + Order = ds.createModel('order', + { + id: { + id: true + }, + date: Date + } + ); + + Store = ds.createModel('store', + { + id: { + id: true, + type: String + }, + state: String + } + ); + + // Relations + Customer.hasMany(Order, {as: 'orders', foreignKey: 'customer_id'}); + Order.belongsTo(Customer, {as: 'customer', foreignKey: 'customer_id'}); + Order.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); + Store.hasMany(Order, {as: 'orders', foreignKey: 'store_id'}); + Store.hasMany(Customer, { + as: 'customers', + through: Order, + foreignKey: 'storeId', + keyThrough: 'customerId' + }); + Customer.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); + Store.hasMany(Customer, {as: 'customers', foreignKey: 'store_id'}); + }); + + it('should map table name', function() { + var table = connector.table('customer'); + expect(table).to.eql('CUSTOMER'); + }); + + it('should map column name', function() { + var column = connector.column('customer', 'name'); + expect(column).to.eql('NAME'); + }); + + it('should find column metadata', function() { + var column = connector.columnMetadata('customer', 'name'); + expect(column).to.eql({ + column: 'NAME', + dataType: 'VARCHAR', + dataLength: 32 + }); + }); + + it('should map property name', function() { + var prop = connector.propertyName('customer', 'NAME'); + expect(prop).to.eql('name'); + }); + + it('should map id column name', function() { + var idCol = connector.idColumn('customer'); + expect(idCol).to.eql('NAME'); + }); + + it('should find escaped id column name', function() { + var idCol = connector.idColumnEscaped('customer'); + expect(idCol).to.eql('`NAME`'); + }); + + it('should find escaped table name', function() { + var table = connector.tableEscaped('customer'); + expect(table).to.eql('`CUSTOMER`'); + }); + + it('should find escaped column name', function() { + var column = connector.columnEscaped('customer', 'vip'); + expect(column).to.eql('`VIP`'); + }); + + it('should find qualified column name', function() { + var column = connector.columnEscaped('customer', 'vip', true); + expect(column).to.eql('`CUSTOMER`.`VIP`'); + }); + + it('should convert to escaped id column value', function() { + var column = connector.idColumnValue('customer', 'John'); + expect(column).to.eql('John'); + }); + + it('builds where', function() { + var where = connector.buildWhere('customer', {name: 'John'}); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `NAME`=?', + params: ['John'] + }); + }); + + it('builds where', function() { + var where = connector.buildWhere('order', {customer: {name: 'John'}}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME`=?', + params: ['John'] + }); + }); + + it('builds qualified where', function() { + var where = connector.buildWhere('customer', {name: 'John'}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME`=?', + params: ['John'] + }); + }); + + it('builds where with null', function() { + var where = connector.buildWhere('customer', {name: null}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` IS NULL', + params: [] + }); + }); + + it('builds where with inq', function() { + var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` IN (?,?)', + params: ['John', 'Mary'] + }); + }); + + it('builds where with or', function() { + var where = connector.buildWhere('customer', + {or: [{name: 'John'}, {name: 'Mary'}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`CUSTOMER`.`NAME`=?)', + params: ['John', 'Mary'] + }); + }); + + it('builds where with or and relation', function() { + var where = connector.buildWhere('customer', + {or: [{name: 'John'}, {store: {state: 'NY'}}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`STORE`.`STATE`=?)', + params: ['John', 'NY'] + }); + }); + + it('builds where with and', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {vip: true}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`CUSTOMER`.`VIP`=?)', + params: ['John', true] + }); + }); + + it('builds where with and + relation', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {store: {state: 'NY'}}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`STORE`.`STATE`=?)', + params: ['John', 'NY'] + }); + }); + + it('builds where with a regexp string that does not have flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: '^J' + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: ['^J'] + }); + }); + + it('builds where with a regexp string that has flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: '^J/i' + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: ['^J/i'] + }); + }); + + it('builds where with a regexp literal that does not have flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: /^J/ + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [/^J/] + }); + }); + + it('builds where with a regexp literal that has flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: /^J/i + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [/^J/i] + }); + }); + + it('builds where with a regexp object that does not have flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: new RegExp(/^J/) + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [/^J/] + }); + }); + + it('builds where with a regexp object that has flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: new RegExp(/^J/i) + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [new RegExp(/^J/i)] + }); + }); + + it('builds where with nesting and/or', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {or: [{vip: true}, {address: null}]}]}, true); + expect(where.toJSON()).to.eql({ + 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 `CUSTOMER`.`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`'); + }); + + 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'); + }); + + it('builds fields for columns', function() { + var fields = connector.buildFields('customer', + {name: 'John', vip: true, unknown: 'Random'}, false, true); + 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( + {sql: '?', params: [true]}); + }); + + it('builds fields for UPDATE without ids', function() { + var fields = connector.buildFieldsForUpdate('customer', + {name: 'John', vip: true}); + expect(fields.toJSON()).to.eql({ + sql: 'SET `CUSTOMER`.`VIP`=?', + params: [true] + }); + }); + + it('builds fields for UPDATE with ids', function() { + var fields = connector.buildFieldsForUpdate('customer', + {name: 'John', vip: true}, false); + expect(fields.toJSON()).to.eql({ + sql: 'SET `CUSTOMER`.`NAME`=?,`CUSTOMER`.`VIP`=?', + params: ['John', true] + }); + }); + + it('builds column names for SELECT', function() { + var cols = connector.buildColumnNames('customer', {}, true); + expect(cols).to + .eql('`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`STORE_ID`'); + }); + + it('builds column names with true fields filter for SELECT', function() { + var cols = connector.buildColumnNames('customer', {fields: {name: true}}, true); + 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}}, true); + expect(cols).to.eql('`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`STORE_ID`'); + }); + + it('builds column names with array fields filter for SELECT', function() { + var cols = connector.buildColumnNames('customer', {fields: ['name']}, true); + 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 `CUSTOMER`.`NAME`=$1', + params: ['John'] + }); + }); + + 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', + params: [false, 'John'] + }); + }); + + it('builds SELECT', 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`.`STORE_ID` FROM `CUSTOMER`' + + ' WHERE `CUSTOMER`.`NAME`=$1 ORDER BY `CUSTOMER`.`NAME` LIMIT 5', + params: ['John'] + }); + }); + + 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)', + params: ['John', true] + }); + }); + + it('normalizes a SQL statement from string', function() { + var sql = 'SELECT * FROM `CUSTOMER`'; + var stmt = new ParameterizedSQL(sql); + expect(stmt.toJSON()).to.eql({sql: sql, params: []}); + }); + + it('normalizes a SQL statement from object without params', function() { + var sql = {sql: 'SELECT * FROM `CUSTOMER`'}; + var stmt = new ParameterizedSQL(sql); + expect(stmt.toJSON()).to.eql({sql: sql.sql, params: []}); + }); + + it('normalizes a SQL statement from object with params', function() { + var sql = + {sql: 'SELECT * FROM `CUSTOMER` WHERE `NAME`=?', params: ['John']}; + var stmt = new ParameterizedSQL(sql); + expect(stmt.toJSON()).to.eql({sql: sql.sql, params: ['John']}); + }); + + it('should throw if the statement is not a string or object', function() { + expect(function() { + /*jshint unused:false */ + var stmt = new ParameterizedSQL(true); + }).to.throw('sql must be a string'); + }); + + it('concats SQL statements', function() { + var stmt1 = {sql: 'SELECT * from `CUSTOMER`'}; + var where = {sql: 'WHERE `NAME`=?', params: ['John']}; + stmt1 = ParameterizedSQL.append(stmt1, where); + expect(stmt1.toJSON()).to.eql( + {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); + }); + + it('concats string SQL statements', function() { + var stmt1 = 'SELECT * from `CUSTOMER`'; + var where = {sql: 'WHERE `NAME`=?', params: ['John']}; + stmt1 = ParameterizedSQL.append(stmt1, where); + expect(stmt1.toJSON()).to.eql( + {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); + }); + + it('should throw if params does not match placeholders', function() { + expect(function() { + var stmt1 = 'SELECT * from `CUSTOMER`'; + var where = {sql: 'WHERE `NAME`=?', params: ['John', 'Mary']}; + stmt1 = ParameterizedSQL.append(stmt1, where); + }).to.throw('must match the number of params'); + }); + + it('should allow execute(sql, callback)', function(done) { + connector.execute('SELECT * FROM `CUSTOMER`', done); + }); + + it('should allow execute(sql, params, callback)', function(done) { + connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', + ['xyz'], done); + }); + + it('should allow execute(sql, params, options, callback)', function(done) { + connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', + ['xyz'], {transaction: true}, done); + }); + + it('should throw if params is not an array for execute()', function() { + expect(function() { + connector.execute('SELECT * FROM `CUSTOMER`', 'xyz', function() { + }); + }).to.throw('params must be an array'); + }); + + it('should throw if options is not an object for execute()', function() { + expect(function() { + connector.execute('SELECT * FROM `CUSTOMER`', [], 'xyz', function() { + }); + }).to.throw('options must be an object'); + }); + + it('should throw if callback is not a function for execute()', function() { + expect(function() { + connector.execute('SELECT * FROM `CUSTOMER`', [], {}, 'xyz'); + }).to.throw('callback must be a function'); + }); + + it('should invoke hooks', function(done) { + var events = []; + connector.observe('before execute', function(ctx, next) { + expect(ctx.req.sql).be.a('string'); + expect(ctx.req.params).be.a('array'); + events.push('before execute'); + next(); + }); + connector.observe('after execute', function(ctx, next) { + expect(ctx.res).be.an('array'); + events.push('after execute'); + next(); + }); + Customer.find(function(err, results) { + expect(events).to.eql(['before execute', 'after execute']); + done(err, results); + }); + }); +}); diff --git a/test/sql.test.js b/test/sql.test.js index dd89e1e7..8a99dfba 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -1,411 +1,411 @@ -var expect = require('chai').expect; -var SQLConnector = require('../lib/sql'); -var ParameterizedSQL = SQLConnector.ParameterizedSQL; -var testConnector = require('./connectors/test-sql-connector'); - -var juggler = require('loopback-datasource-juggler'); -var ds = new juggler.DataSource({ - connector: testConnector, - debug: true -}); -var connector; -var Customer; - -describe('sql connector', function() { - before(function() { - connector = ds.connector; - connector._tables = {}; - connector._models = {}; - Customer = ds.createModel('customer', - { - name: { - id: true, - type: String, - testdb: { - column: 'NAME', - dataType: 'VARCHAR', - dataLength: 32 - } - }, vip: { - type: Boolean, - testdb: { - column: 'VIP' - } - }, - address: String - }, - {testdb: {table: 'CUSTOMER'}}); - }); - - it('should map table name', function() { - var table = connector.table('customer'); - expect(table).to.eql('CUSTOMER'); - }); - - it('should map column name', function() { - var column = connector.column('customer', 'name'); - expect(column).to.eql('NAME'); - }); - - it('should find column metadata', function() { - var column = connector.columnMetadata('customer', 'name'); - expect(column).to.eql({ - column: 'NAME', - dataType: 'VARCHAR', - dataLength: 32 - }); - }); - - it('should map property name', function() { - var prop = connector.propertyName('customer', 'NAME'); - expect(prop).to.eql('name'); - }); - - it('should map id column name', function() { - var idCol = connector.idColumn('customer'); - expect(idCol).to.eql('NAME'); - }); - - it('should find escaped id column name', function() { - var idCol = connector.idColumnEscaped('customer'); - expect(idCol).to.eql('`NAME`'); - }); - - it('should find escaped table name', function() { - var table = connector.tableEscaped('customer'); - expect(table).to.eql('`CUSTOMER`'); - }); - - it('should find escaped column name', function() { - var column = connector.columnEscaped('customer', 'vip'); - expect(column).to.eql('`VIP`'); - }); - - it('should convert to escaped id column value', function() { - var column = connector.idColumnValue('customer', 'John'); - expect(column).to.eql('John'); - }); - - it('builds where', function() { - var where = connector.buildWhere('customer', {name: 'John'}); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME`=?', - params: ['John'] - }); - }); - - it('builds where with null', function() { - var where = connector.buildWhere('customer', {name: null}); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` IS NULL', - params: [] - }); - }); - - it('builds where with inq', function() { - var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` IN (?,?)', - params: ['John', 'Mary'] - }); - }); - - it('builds where with or', function() { - var where = connector.buildWhere('customer', - {or: [{name: 'John'}, {name: 'Mary'}]}); - expect(where.toJSON()).to.eql({ - sql: 'WHERE (`NAME`=?) OR (`NAME`=?)', - params: ['John', 'Mary'] - }); - }); - - it('builds where with and', function() { - var where = connector.buildWhere('customer', - {and: [{name: 'John'}, {vip: true}]}); - expect(where.toJSON()).to.eql({ - sql: 'WHERE (`NAME`=?) AND (`VIP`=?)', - params: ['John', true] - }); - }); - - it('builds where with a regexp string that does not have flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: '^J' - } - }); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', - params: ['^J'] - }); - }); - - it('builds where with a regexp string that has flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: '^J/i' - } - }); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', - params: ['^J/i'] - }); - }); - - it('builds where with a regexp literal that does not have flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: /^J/ - } - }); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', - params: [/^J/] - }); - }); - - it('builds where with a regexp literal that has flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: /^J/i - } - }); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', - params: [/^J/i] - }); - }); - - it('builds where with a regexp object that does not have flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: new RegExp(/^J/) - } - }); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', - params: [/^J/] - }); - }); - - it('builds where with a regexp object that has flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: new RegExp(/^J/i) - } - }); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME` REGEXP ?', - params: [new RegExp(/^J/i)] - }); - }); - - it('builds where with nesting and/or', 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))', - params: ['John', true] - }); - }); - - it('builds order by with one field', function() { - var orderBy = connector.buildOrderBy('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 `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 `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(['`NAME`', '`VIP`']); - expect(fields.columnValues[0].toJSON()).to.eql( - {sql: '?', params: ['John']}); - expect(fields.columnValues[1].toJSON()).to.eql( - {sql: '?', params: [true]}); - }); - - it('builds fields for UPDATE without ids', function() { - var fields = connector.buildFieldsForUpdate('customer', - {name: 'John', vip: true}); - expect(fields.toJSON()).to.eql({ - sql: 'SET `VIP`=?', - params: [true] - }); - }); - - it('builds fields for UPDATE with ids', function() { - var fields = connector.buildFieldsForUpdate('customer', - {name: 'John', vip: true}, false); - expect(fields.toJSON()).to.eql({ - sql: 'SET `NAME`=?,`VIP`=?', - params: ['John', true] - }); - }); - - it('builds column names for SELECT', function() { - var cols = connector.buildColumnNames('customer'); - expect(cols).to.eql('`NAME`,`VIP`,`ADDRESS`'); - }); - - it('builds column names with true fields filter for SELECT', function() { - var cols = connector.buildColumnNames('customer', {fields: {name: true}}); - 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('`VIP`,`ADDRESS`'); - }); - - it('builds column names with array fields filter for SELECT', function() { - var cols = connector.buildColumnNames('customer', {fields: ['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 `NAME`=$1', - params: ['John'] - }); - }); - - 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', - params: [false, 'John'] - }); - }); - - it('builds SELECT', function() { - var sql = connector.buildSelect('customer', - {order: 'name', limit: 5, where: {name: 'John'}}); - expect(sql.toJSON()).to.eql({ - sql: 'SELECT `NAME`,`VIP`,`ADDRESS` FROM `CUSTOMER`' + - ' WHERE `NAME`=$1 ORDER BY `NAME` LIMIT 5', - params: ['John'] - }); - }); - - 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)', - params: ['John', true] - }); - }); - - it('normalizes a SQL statement from string', function() { - var sql = 'SELECT * FROM `CUSTOMER`'; - var stmt = new ParameterizedSQL(sql); - expect(stmt.toJSON()).to.eql({sql: sql, params: []}); - }); - - it('normalizes a SQL statement from object without params', function() { - var sql = {sql: 'SELECT * FROM `CUSTOMER`'}; - var stmt = new ParameterizedSQL(sql); - expect(stmt.toJSON()).to.eql({sql: sql.sql, params: []}); - }); - - it('normalizes a SQL statement from object with params', function() { - var sql = - {sql: 'SELECT * FROM `CUSTOMER` WHERE `NAME`=?', params: ['John']}; - var stmt = new ParameterizedSQL(sql); - expect(stmt.toJSON()).to.eql({sql: sql.sql, params: ['John']}); - }); - - it('should throw if the statement is not a string or object', function() { - expect(function() { - /*jshint unused:false */ - var stmt = new ParameterizedSQL(true); - }).to.throw('sql must be a string'); - }); - - it('concats SQL statements', function() { - var stmt1 = {sql: 'SELECT * from `CUSTOMER`'}; - var where = {sql: 'WHERE `NAME`=?', params: ['John']}; - stmt1 = ParameterizedSQL.append(stmt1, where); - expect(stmt1.toJSON()).to.eql( - {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); - }); - - it('concats string SQL statements', function() { - var stmt1 = 'SELECT * from `CUSTOMER`'; - var where = {sql: 'WHERE `NAME`=?', params: ['John']}; - stmt1 = ParameterizedSQL.append(stmt1, where); - expect(stmt1.toJSON()).to.eql( - {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); - }); - - it('should throw if params does not match placeholders', function() { - expect(function() { - var stmt1 = 'SELECT * from `CUSTOMER`'; - var where = {sql: 'WHERE `NAME`=?', params: ['John', 'Mary']}; - stmt1 = ParameterizedSQL.append(stmt1, where); - }).to.throw('must match the number of params'); - }); - - it('should allow execute(sql, callback)', function(done) { - connector.execute('SELECT * FROM `CUSTOMER`', done); - }); - - it('should allow execute(sql, params, callback)', function(done) { - connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', - ['xyz'], done); - }); - - it('should allow execute(sql, params, options, callback)', function(done) { - connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', - ['xyz'], {transaction: true}, done); - }); - - it('should throw if params is not an array for execute()', function() { - expect(function() { - connector.execute('SELECT * FROM `CUSTOMER`', 'xyz', function() { - }); - }).to.throw('params must be an array'); - }); - - it('should throw if options is not an object for execute()', function() { - expect(function() { - connector.execute('SELECT * FROM `CUSTOMER`', [], 'xyz', function() { - }); - }).to.throw('options must be an object'); - }); - - it('should throw if callback is not a function for execute()', function() { - expect(function() { - connector.execute('SELECT * FROM `CUSTOMER`', [], {}, 'xyz'); - }).to.throw('callback must be a function'); - }); - - it('should invoke hooks', function(done) { - var events = []; - connector.observe('before execute', function(ctx, next) { - expect(ctx.req.sql).be.a('string'); - expect(ctx.req.params).be.a('array'); - events.push('before execute'); - next(); - }); - connector.observe('after execute', function(ctx, next) { - expect(ctx.res).be.an('array'); - events.push('after execute'); - next(); - }); - Customer.find(function(err, results) { - expect(events).to.eql(['before execute', 'after execute']); - done(err, results); - }); - }); -}); +//var expect = require('chai').expect; +//var SQLConnector = require('../lib/sql'); +//var ParameterizedSQL = SQLConnector.ParameterizedSQL; +//var testConnector = require('./connectors/test-sql-connector'); + +//var juggler = require('loopback-datasource-juggler'); +//var ds = new juggler.DataSource({ + //connector: testConnector, + //debug: true +//}); +//var connector; +//var Customer; + +//describe('sql connector', function() { + //before(function() { + //connector = ds.connector; + //connector._tables = {}; + //connector._models = {}; + //Customer = ds.createModel('customer', + //{ + //name: { + //id: true, + //type: String, + //testdb: { + //column: 'NAME', + //dataType: 'VARCHAR', + //dataLength: 32 + //} + //}, vip: { + //type: Boolean, + //testdb: { + //column: 'VIP' + //} + //}, + //address: String + //}, + //{testdb: {table: 'CUSTOMER'}}); + //}); + + //it('should map table name', function() { + //var table = connector.table('customer'); + //expect(table).to.eql('CUSTOMER'); + //}); + + //it('should map column name', function() { + //var column = connector.column('customer', 'name'); + //expect(column).to.eql('NAME'); + //}); + + //it('should find column metadata', function() { + //var column = connector.columnMetadata('customer', 'name'); + //expect(column).to.eql({ + //column: 'NAME', + //dataType: 'VARCHAR', + //dataLength: 32 + //}); + //}); + + //it('should map property name', function() { + //var prop = connector.propertyName('customer', 'NAME'); + //expect(prop).to.eql('name'); + //}); + + //it('should map id column name', function() { + //var idCol = connector.idColumn('customer'); + //expect(idCol).to.eql('NAME'); + //}); + + //it('should find escaped id column name', function() { + //var idCol = connector.idColumnEscaped('customer'); + //expect(idCol).to.eql('`NAME`'); + //}); + + //it('should find escaped table name', function() { + //var table = connector.tableEscaped('customer'); + //expect(table).to.eql('`CUSTOMER`'); + //}); + + //it('should find escaped column name', function() { + //var column = connector.columnEscaped('customer', 'vip'); + //expect(column).to.eql('`VIP`'); + //}); + + //it('should convert to escaped id column value', function() { + //var column = connector.idColumnValue('customer', 'John'); + //expect(column).to.eql('John'); + //}); + + //it('builds where', function() { + //var where = connector.buildWhere('customer', {name: 'John'}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME`=?', + //params: ['John'] + //}); + //}); + + //it('builds where with null', function() { + //var where = connector.buildWhere('customer', {name: null}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` IS NULL', + //params: [] + //}); + //}); + + //it('builds where with inq', function() { + //var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` IN (?,?)', + //params: ['John', 'Mary'] + //}); + //}); + + //it('builds where with or', function() { + //var where = connector.buildWhere('customer', + //{or: [{name: 'John'}, {name: 'Mary'}]}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE (`NAME`=?) OR (`NAME`=?)', + //params: ['John', 'Mary'] + //}); + //}); + + //it('builds where with and', function() { + //var where = connector.buildWhere('customer', + //{and: [{name: 'John'}, {vip: true}]}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE (`NAME`=?) AND (`VIP`=?)', + //params: ['John', true] + //}); + //}); + + //it('builds where with a regexp string that does not have flags', function() { + //var where = connector.buildWhere('customer', { + //name: { + //regexp: '^J' + //} + //}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` REGEXP ?', + //params: ['^J'] + //}); + //}); + + //it('builds where with a regexp string that has flags', function() { + //var where = connector.buildWhere('customer', { + //name: { + //regexp: '^J/i' + //} + //}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` REGEXP ?', + //params: ['^J/i'] + //}); + //}); + + //it('builds where with a regexp literal that does not have flags', function() { + //var where = connector.buildWhere('customer', { + //name: { + //regexp: /^J/ + //} + //}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` REGEXP ?', + //params: [/^J/] + //}); + //}); + + //it('builds where with a regexp literal that has flags', function() { + //var where = connector.buildWhere('customer', { + //name: { + //regexp: /^J/i + //} + //}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` REGEXP ?', + //params: [/^J/i] + //}); + //}); + + //it('builds where with a regexp object that does not have flags', function() { + //var where = connector.buildWhere('customer', { + //name: { + //regexp: new RegExp(/^J/) + //} + //}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` REGEXP ?', + //params: [/^J/] + //}); + //}); + + //it('builds where with a regexp object that has flags', function() { + //var where = connector.buildWhere('customer', { + //name: { + //regexp: new RegExp(/^J/i) + //} + //}); + //expect(where.toJSON()).to.eql({ + //sql: 'WHERE `NAME` REGEXP ?', + //params: [new RegExp(/^J/i)] + //}); + //}); + + //it('builds where with nesting and/or', 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))', + //params: ['John', true] + //}); + //}); + + //it('builds order by with one field', function() { + //var orderBy = connector.buildOrderBy('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 `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 `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(['`NAME`', '`VIP`']); + //expect(fields.columnValues[0].toJSON()).to.eql( + //{sql: '?', params: ['John']}); + //expect(fields.columnValues[1].toJSON()).to.eql( + //{sql: '?', params: [true]}); + //}); + + //it('builds fields for UPDATE without ids', function() { + //var fields = connector.buildFieldsForUpdate('customer', + //{name: 'John', vip: true}); + //expect(fields.toJSON()).to.eql({ + //sql: 'SET `VIP`=?', + //params: [true] + //}); + //}); + + //it('builds fields for UPDATE with ids', function() { + //var fields = connector.buildFieldsForUpdate('customer', + //{name: 'John', vip: true}, false); + //expect(fields.toJSON()).to.eql({ + //sql: 'SET `NAME`=?,`VIP`=?', + //params: ['John', true] + //}); + //}); + + //it('builds column names for SELECT', function() { + //var cols = connector.buildColumnNames('customer'); + //expect(cols).to.eql('`NAME`,`VIP`,`ADDRESS`'); + //}); + + //it('builds column names with true fields filter for SELECT', function() { + //var cols = connector.buildColumnNames('customer', {fields: {name: true}}); + //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('`VIP`,`ADDRESS`'); + //}); + + //it('builds column names with array fields filter for SELECT', function() { + //var cols = connector.buildColumnNames('customer', {fields: ['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 `NAME`=$1', + //params: ['John'] + //}); + //}); + + //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', + //params: [false, 'John'] + //}); + //}); + + //it('builds SELECT', function() { + //var sql = connector.buildSelect('customer', + //{order: 'name', limit: 5, where: {name: 'John'}}); + //expect(sql.toJSON()).to.eql({ + //sql: 'SELECT `NAME`,`VIP`,`ADDRESS` FROM `CUSTOMER`' + + //' WHERE `NAME`=$1 ORDER BY `NAME` LIMIT 5', + //params: ['John'] + //}); + //}); + + //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)', + //params: ['John', true] + //}); + //}); + + //it('normalizes a SQL statement from string', function() { + //var sql = 'SELECT * FROM `CUSTOMER`'; + //var stmt = new ParameterizedSQL(sql); + //expect(stmt.toJSON()).to.eql({sql: sql, params: []}); + //}); + + //it('normalizes a SQL statement from object without params', function() { + //var sql = {sql: 'SELECT * FROM `CUSTOMER`'}; + //var stmt = new ParameterizedSQL(sql); + //expect(stmt.toJSON()).to.eql({sql: sql.sql, params: []}); + //}); + + //it('normalizes a SQL statement from object with params', function() { + //var sql = + //{sql: 'SELECT * FROM `CUSTOMER` WHERE `NAME`=?', params: ['John']}; + //var stmt = new ParameterizedSQL(sql); + //expect(stmt.toJSON()).to.eql({sql: sql.sql, params: ['John']}); + //}); + + //it('should throw if the statement is not a string or object', function() { + //expect(function() { + //[>jshint unused:false <] + //var stmt = new ParameterizedSQL(true); + //}).to.throw('sql must be a string'); + //}); + + //it('concats SQL statements', function() { + //var stmt1 = {sql: 'SELECT * from `CUSTOMER`'}; + //var where = {sql: 'WHERE `NAME`=?', params: ['John']}; + //stmt1 = ParameterizedSQL.append(stmt1, where); + //expect(stmt1.toJSON()).to.eql( + //{sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); + //}); + + //it('concats string SQL statements', function() { + //var stmt1 = 'SELECT * from `CUSTOMER`'; + //var where = {sql: 'WHERE `NAME`=?', params: ['John']}; + //stmt1 = ParameterizedSQL.append(stmt1, where); + //expect(stmt1.toJSON()).to.eql( + //{sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); + //}); + + //it('should throw if params does not match placeholders', function() { + //expect(function() { + //var stmt1 = 'SELECT * from `CUSTOMER`'; + //var where = {sql: 'WHERE `NAME`=?', params: ['John', 'Mary']}; + //stmt1 = ParameterizedSQL.append(stmt1, where); + //}).to.throw('must match the number of params'); + //}); + + //it('should allow execute(sql, callback)', function(done) { + //connector.execute('SELECT * FROM `CUSTOMER`', done); + //}); + + //it('should allow execute(sql, params, callback)', function(done) { + //connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', + //['xyz'], done); + //}); + + //it('should allow execute(sql, params, options, callback)', function(done) { + //connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', + //['xyz'], {transaction: true}, done); + //}); + + //it('should throw if params is not an array for execute()', function() { + //expect(function() { + //connector.execute('SELECT * FROM `CUSTOMER`', 'xyz', function() { + //}); + //}).to.throw('params must be an array'); + //}); + + //it('should throw if options is not an object for execute()', function() { + //expect(function() { + //connector.execute('SELECT * FROM `CUSTOMER`', [], 'xyz', function() { + //}); + //}).to.throw('options must be an object'); + //}); + + //it('should throw if callback is not a function for execute()', function() { + //expect(function() { + //connector.execute('SELECT * FROM `CUSTOMER`', [], {}, 'xyz'); + //}).to.throw('callback must be a function'); + //}); + + //it('should invoke hooks', function(done) { + //var events = []; + //connector.observe('before execute', function(ctx, next) { + //expect(ctx.req.sql).be.a('string'); + //expect(ctx.req.params).be.a('array'); + //events.push('before execute'); + //next(); + //}); + //connector.observe('after execute', function(ctx, next) { + //expect(ctx.res).be.an('array'); + //events.push('after execute'); + //next(); + //}); + //Customer.find(function(err, results) { + //expect(events).to.eql(['before execute', 'after execute']); + //done(err, results); + //}); + //}); +//}); From 7de9b4dec2a9d8277f606257215d9032a8989b7c Mon Sep 17 00:00:00 2001 From: kolach Date: Tue, 22 Mar 2016 20:24:20 -0600 Subject: [PATCH 05/12] order and where statements for belongsTo models --- lib/sql.js | 169 ++++++- test/sql.another.test.js | 489 ------------------- test/sql.test.js | 985 +++++++++++++++++++++++---------------- 3 files changed, 729 insertions(+), 914 deletions(-) delete mode 100644 test/sql.another.test.js diff --git a/lib/sql.js b/lib/sql.js index e51f8c2d..8c65ba10 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -653,6 +653,109 @@ SQLConnector.prototype.buildWhere = function(model, where, qualified) { return whereClause; }; + +/** + * Build the inner joins for the where object + * @param {string} model Model name + * @param {object|array|string} order order by clouse + * @returns {array} Array of relations to make joins + */ +SQLConnector.prototype._innerRelations = function(model, where) { + var result = []; + var relations = this.getModelDefinition(model).model.relations; + if (relations) { + for (var key in where) { + if (key in relations) { + var rel = relations[key]; + var modelTo = rel.modelTo.definition.name; + result.push(rel); + result = result.concat(this._innerRelations(modelTo, where[key])); + } + } + } + return result; +}; + +/** + * Build the outer joins for the where object + * @param {string} model Model name + * @param {object|array|string} order order by clouse + * @returns {array} Array of relations + */ +SQLConnector.prototype._outerRelations = function(model, orders) { + var result = []; + + + var self = this; + var relations = this.getModelDefinition(model).model.relations; + + if (!Array.isArray(orders)) { + orders = [orders]; + } + + for (var i = 0; i < orders.length; i++) { + var order = orders[i]; + if (typeof order === 'object') { + for (var key in order) { + if (key in relations) { + var rel = relations[key]; + result.push(rel); + var modelTo = rel.modelTo.definition.name; + result = result.concat(self._outerRelations(modelTo, order[key])); + } + } + } + } + + return result; +}; + + + + +/** + * Build the SQL JOIN clauses for planed joins array + * @param {string} model Model name + * @param {object} where where clause + * @param {string|object|array} order order clause + * @returns {string} The SQL JOIN clauses, or null if not applicable + */ +SQLConnector.prototype.buildJoins = function(model, where, order) { + + var self = this; + + function makeJoinStmt(isInner, rel) { + var joinType = isInner ? 'INNER JOIN ' : 'LEFT OUTER JOIN '; + var modelFrom = rel.modelFrom.definition.name; + var modelTo = rel.modelTo.definition.name; + var keyFrom = rel.keyFrom; + var keyTo = rel.keyTo; + return joinType + self.tableEscaped(modelTo) + ' ON ' + + self.columnEscaped(modelFrom, keyFrom, true) + '=' + + self.columnEscaped(modelTo, keyTo, true); + } + + var joinStms = []; + + var inner = this._innerRelations(model, where); + var outer = this._outerRelations(model, order); + if (inner.length + outer.length === 0) { + return null; + } + + for (var i = 0; i < inner.length; i++) { + joinStms.push(makeJoinStmt(true, inner[i])); + } + + for (var j = 0; j < outer.length; j++) { + if (inner.indexOf(outer[j]) === -1) { + joinStms.push(makeJoinStmt(false, outer[j])); + } + } + + return joinStms.join(' '); +}; + /** * Build SQL expression * @param {String} columnName Escaped column name @@ -859,29 +962,60 @@ SQLConnector.prototype._buildWhere = function(model, where, qualified) { return whereStmt; }; + /** - * Build the ORDER BY clause + * Build a collection of order by columns * @param {string} model Model name - * @param {string[]} order An array of sorting criteria - * @returns {string} The ORDER BY clause + * @param {string|array|object} order An array of sorting criteria + * @returns {array} Array of columns with [asc|desc] to make ORDER BY clause */ -SQLConnector.prototype.buildOrderBy = function(model, order) { - if (!order) { - return ''; +SQLConnector.prototype._buildOrderBy = function(model, orders) { + if (!orders) { + return null; } + var self = this; - if (typeof order === 'string') { - order = [order]; - } + var relations = this.getModelDefinition(model).model.relations; var clauses = []; - for (var i = 0, n = order.length; i < n; i++) { - var t = order[i].split(/[\s,]+/); - if (t.length === 1) { - clauses.push(self.columnEscaped(model, order[i], true)); + + if (!Array.isArray(orders)) { + orders = [orders]; + } + + for (var i = 0; i < orders.length; i++) { + var order = orders[i]; + if (typeof order === 'object') { + for (var key in order) { + if (key in relations) { + var rel = relations[key]; + var modelTo = rel.modelTo.definition.name; + var subClouses = self._buildOrderBy(modelTo, order[key]); + if (subClouses) { + clauses = clauses.concat(subClouses); + } + } + } } else { - clauses.push(self.columnEscaped(model, t[0], true) + ' ' + t[1]); + var t = order.split(/[\s,]+/); + if (t.length === 1) { + clauses.push(self.columnEscaped(model, order, true)); + } else { + clauses.push(self.columnEscaped(model, t[0], true) + ' ' + t[1]); + } } } + + return clauses; +}; + +/** + * Build the ORDER BY clause + * @param {string} model Model name + * @param {string[]} order An array of sorting criteria + * @returns {string} The ORDER BY clause + */ +SQLConnector.prototype.buildOrderBy = function(model, order) { + var clauses = this._buildOrderBy(model, order); return 'ORDER BY ' + clauses.join(','); }; @@ -1019,6 +1153,13 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { if (filter) { + if (filter.where || filter.order) { + var joinsStmt = this.buildJoins(model, filter.where, filter.order); + if (joinsStmt) { + selectStmt.merge(joinsStmt); + } + } + if (filter.where) { var whereStmt = this.buildWhere(model, filter.where, true); selectStmt.merge(whereStmt); diff --git a/test/sql.another.test.js b/test/sql.another.test.js deleted file mode 100644 index 1647352f..00000000 --- a/test/sql.another.test.js +++ /dev/null @@ -1,489 +0,0 @@ -var expect = require('chai').expect; -var SQLConnector = require('../lib/sql'); -var ParameterizedSQL = SQLConnector.ParameterizedSQL; -var testConnector = require('./connectors/test-sql-connector'); - -var juggler = require('loopback-datasource-juggler'); -var ds = new juggler.DataSource({ - connector: testConnector, - debug: true -}); -var connector; -var Customer; -var Order; -var Store; - -describe('sql connector', function() { - before(function() { - connector = ds.connector; - connector._tables = {}; - connector._models = {}; - Customer = ds.createModel('customer', - { - name: { - id: true, - type: String, - testdb: { - column: 'NAME', - dataType: 'VARCHAR', - dataLength: 32 - } - }, - vip: { - type: Boolean, - testdb: { - column: 'VIP' - } - }, - address: String - }, - {testdb: {table: 'CUSTOMER'}} - ); - - Order = ds.createModel('order', - { - id: { - id: true - }, - date: Date - } - ); - - Store = ds.createModel('store', - { - id: { - id: true, - type: String - }, - state: String - } - ); - - // Relations - Customer.hasMany(Order, {as: 'orders', foreignKey: 'customer_id'}); - Order.belongsTo(Customer, {as: 'customer', foreignKey: 'customer_id'}); - Order.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); - Store.hasMany(Order, {as: 'orders', foreignKey: 'store_id'}); - Store.hasMany(Customer, { - as: 'customers', - through: Order, - foreignKey: 'storeId', - keyThrough: 'customerId' - }); - Customer.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); - Store.hasMany(Customer, {as: 'customers', foreignKey: 'store_id'}); - }); - - it('should map table name', function() { - var table = connector.table('customer'); - expect(table).to.eql('CUSTOMER'); - }); - - it('should map column name', function() { - var column = connector.column('customer', 'name'); - expect(column).to.eql('NAME'); - }); - - it('should find column metadata', function() { - var column = connector.columnMetadata('customer', 'name'); - expect(column).to.eql({ - column: 'NAME', - dataType: 'VARCHAR', - dataLength: 32 - }); - }); - - it('should map property name', function() { - var prop = connector.propertyName('customer', 'NAME'); - expect(prop).to.eql('name'); - }); - - it('should map id column name', function() { - var idCol = connector.idColumn('customer'); - expect(idCol).to.eql('NAME'); - }); - - it('should find escaped id column name', function() { - var idCol = connector.idColumnEscaped('customer'); - expect(idCol).to.eql('`NAME`'); - }); - - it('should find escaped table name', function() { - var table = connector.tableEscaped('customer'); - expect(table).to.eql('`CUSTOMER`'); - }); - - it('should find escaped column name', function() { - var column = connector.columnEscaped('customer', 'vip'); - expect(column).to.eql('`VIP`'); - }); - - it('should find qualified column name', function() { - var column = connector.columnEscaped('customer', 'vip', true); - expect(column).to.eql('`CUSTOMER`.`VIP`'); - }); - - it('should convert to escaped id column value', function() { - var column = connector.idColumnValue('customer', 'John'); - expect(column).to.eql('John'); - }); - - it('builds where', function() { - var where = connector.buildWhere('customer', {name: 'John'}); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `NAME`=?', - params: ['John'] - }); - }); - - it('builds where', function() { - var where = connector.buildWhere('order', {customer: {name: 'John'}}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME`=?', - params: ['John'] - }); - }); - - it('builds qualified where', function() { - var where = connector.buildWhere('customer', {name: 'John'}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME`=?', - params: ['John'] - }); - }); - - it('builds where with null', function() { - var where = connector.buildWhere('customer', {name: null}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` IS NULL', - params: [] - }); - }); - - it('builds where with inq', function() { - var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` IN (?,?)', - params: ['John', 'Mary'] - }); - }); - - it('builds where with or', function() { - var where = connector.buildWhere('customer', - {or: [{name: 'John'}, {name: 'Mary'}]}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`CUSTOMER`.`NAME`=?)', - params: ['John', 'Mary'] - }); - }); - - it('builds where with or and relation', function() { - var where = connector.buildWhere('customer', - {or: [{name: 'John'}, {store: {state: 'NY'}}]}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`STORE`.`STATE`=?)', - params: ['John', 'NY'] - }); - }); - - it('builds where with and', function() { - var where = connector.buildWhere('customer', - {and: [{name: 'John'}, {vip: true}]}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`CUSTOMER`.`VIP`=?)', - params: ['John', true] - }); - }); - - it('builds where with and + relation', function() { - var where = connector.buildWhere('customer', - {and: [{name: 'John'}, {store: {state: 'NY'}}]}, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`STORE`.`STATE`=?)', - params: ['John', 'NY'] - }); - }); - - it('builds where with a regexp string that does not have flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: '^J' - } - }, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', - params: ['^J'] - }); - }); - - it('builds where with a regexp string that has flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: '^J/i' - } - }, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', - params: ['^J/i'] - }); - }); - - it('builds where with a regexp literal that does not have flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: /^J/ - } - }, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', - params: [/^J/] - }); - }); - - it('builds where with a regexp literal that has flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: /^J/i - } - }, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', - params: [/^J/i] - }); - }); - - it('builds where with a regexp object that does not have flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: new RegExp(/^J/) - } - }, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', - params: [/^J/] - }); - }); - - it('builds where with a regexp object that has flags', function() { - var where = connector.buildWhere('customer', { - name: { - regexp: new RegExp(/^J/i) - } - }, true); - expect(where.toJSON()).to.eql({ - sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', - params: [new RegExp(/^J/i)] - }); - }); - - it('builds where with nesting and/or', function() { - var where = connector.buildWhere('customer', - {and: [{name: 'John'}, {or: [{vip: true}, {address: null}]}]}, true); - expect(where.toJSON()).to.eql({ - 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 `CUSTOMER`.`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`'); - }); - - 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'); - }); - - it('builds fields for columns', function() { - var fields = connector.buildFields('customer', - {name: 'John', vip: true, unknown: 'Random'}, false, true); - 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( - {sql: '?', params: [true]}); - }); - - it('builds fields for UPDATE without ids', function() { - var fields = connector.buildFieldsForUpdate('customer', - {name: 'John', vip: true}); - expect(fields.toJSON()).to.eql({ - sql: 'SET `CUSTOMER`.`VIP`=?', - params: [true] - }); - }); - - it('builds fields for UPDATE with ids', function() { - var fields = connector.buildFieldsForUpdate('customer', - {name: 'John', vip: true}, false); - expect(fields.toJSON()).to.eql({ - sql: 'SET `CUSTOMER`.`NAME`=?,`CUSTOMER`.`VIP`=?', - params: ['John', true] - }); - }); - - it('builds column names for SELECT', function() { - var cols = connector.buildColumnNames('customer', {}, true); - expect(cols).to - .eql('`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`STORE_ID`'); - }); - - it('builds column names with true fields filter for SELECT', function() { - var cols = connector.buildColumnNames('customer', {fields: {name: true}}, true); - 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}}, true); - expect(cols).to.eql('`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`STORE_ID`'); - }); - - it('builds column names with array fields filter for SELECT', function() { - var cols = connector.buildColumnNames('customer', {fields: ['name']}, true); - 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 `CUSTOMER`.`NAME`=$1', - params: ['John'] - }); - }); - - 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', - params: [false, 'John'] - }); - }); - - it('builds SELECT', 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`.`STORE_ID` FROM `CUSTOMER`' + - ' WHERE `CUSTOMER`.`NAME`=$1 ORDER BY `CUSTOMER`.`NAME` LIMIT 5', - params: ['John'] - }); - }); - - 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)', - params: ['John', true] - }); - }); - - it('normalizes a SQL statement from string', function() { - var sql = 'SELECT * FROM `CUSTOMER`'; - var stmt = new ParameterizedSQL(sql); - expect(stmt.toJSON()).to.eql({sql: sql, params: []}); - }); - - it('normalizes a SQL statement from object without params', function() { - var sql = {sql: 'SELECT * FROM `CUSTOMER`'}; - var stmt = new ParameterizedSQL(sql); - expect(stmt.toJSON()).to.eql({sql: sql.sql, params: []}); - }); - - it('normalizes a SQL statement from object with params', function() { - var sql = - {sql: 'SELECT * FROM `CUSTOMER` WHERE `NAME`=?', params: ['John']}; - var stmt = new ParameterizedSQL(sql); - expect(stmt.toJSON()).to.eql({sql: sql.sql, params: ['John']}); - }); - - it('should throw if the statement is not a string or object', function() { - expect(function() { - /*jshint unused:false */ - var stmt = new ParameterizedSQL(true); - }).to.throw('sql must be a string'); - }); - - it('concats SQL statements', function() { - var stmt1 = {sql: 'SELECT * from `CUSTOMER`'}; - var where = {sql: 'WHERE `NAME`=?', params: ['John']}; - stmt1 = ParameterizedSQL.append(stmt1, where); - expect(stmt1.toJSON()).to.eql( - {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); - }); - - it('concats string SQL statements', function() { - var stmt1 = 'SELECT * from `CUSTOMER`'; - var where = {sql: 'WHERE `NAME`=?', params: ['John']}; - stmt1 = ParameterizedSQL.append(stmt1, where); - expect(stmt1.toJSON()).to.eql( - {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); - }); - - it('should throw if params does not match placeholders', function() { - expect(function() { - var stmt1 = 'SELECT * from `CUSTOMER`'; - var where = {sql: 'WHERE `NAME`=?', params: ['John', 'Mary']}; - stmt1 = ParameterizedSQL.append(stmt1, where); - }).to.throw('must match the number of params'); - }); - - it('should allow execute(sql, callback)', function(done) { - connector.execute('SELECT * FROM `CUSTOMER`', done); - }); - - it('should allow execute(sql, params, callback)', function(done) { - connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', - ['xyz'], done); - }); - - it('should allow execute(sql, params, options, callback)', function(done) { - connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', - ['xyz'], {transaction: true}, done); - }); - - it('should throw if params is not an array for execute()', function() { - expect(function() { - connector.execute('SELECT * FROM `CUSTOMER`', 'xyz', function() { - }); - }).to.throw('params must be an array'); - }); - - it('should throw if options is not an object for execute()', function() { - expect(function() { - connector.execute('SELECT * FROM `CUSTOMER`', [], 'xyz', function() { - }); - }).to.throw('options must be an object'); - }); - - it('should throw if callback is not a function for execute()', function() { - expect(function() { - connector.execute('SELECT * FROM `CUSTOMER`', [], {}, 'xyz'); - }).to.throw('callback must be a function'); - }); - - it('should invoke hooks', function(done) { - var events = []; - connector.observe('before execute', function(ctx, next) { - expect(ctx.req.sql).be.a('string'); - expect(ctx.req.params).be.a('array'); - events.push('before execute'); - next(); - }); - connector.observe('after execute', function(ctx, next) { - expect(ctx.res).be.an('array'); - events.push('after execute'); - next(); - }); - Customer.find(function(err, results) { - expect(events).to.eql(['before execute', 'after execute']); - done(err, results); - }); - }); -}); diff --git a/test/sql.test.js b/test/sql.test.js index 8a99dfba..f858c2db 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -1,411 +1,574 @@ -//var expect = require('chai').expect; -//var SQLConnector = require('../lib/sql'); -//var ParameterizedSQL = SQLConnector.ParameterizedSQL; -//var testConnector = require('./connectors/test-sql-connector'); - -//var juggler = require('loopback-datasource-juggler'); -//var ds = new juggler.DataSource({ - //connector: testConnector, - //debug: true -//}); -//var connector; -//var Customer; - -//describe('sql connector', function() { - //before(function() { - //connector = ds.connector; - //connector._tables = {}; - //connector._models = {}; - //Customer = ds.createModel('customer', - //{ - //name: { - //id: true, - //type: String, - //testdb: { - //column: 'NAME', - //dataType: 'VARCHAR', - //dataLength: 32 - //} - //}, vip: { - //type: Boolean, - //testdb: { - //column: 'VIP' - //} - //}, - //address: String - //}, - //{testdb: {table: 'CUSTOMER'}}); - //}); - - //it('should map table name', function() { - //var table = connector.table('customer'); - //expect(table).to.eql('CUSTOMER'); - //}); - - //it('should map column name', function() { - //var column = connector.column('customer', 'name'); - //expect(column).to.eql('NAME'); - //}); - - //it('should find column metadata', function() { - //var column = connector.columnMetadata('customer', 'name'); - //expect(column).to.eql({ - //column: 'NAME', - //dataType: 'VARCHAR', - //dataLength: 32 - //}); - //}); - - //it('should map property name', function() { - //var prop = connector.propertyName('customer', 'NAME'); - //expect(prop).to.eql('name'); - //}); - - //it('should map id column name', function() { - //var idCol = connector.idColumn('customer'); - //expect(idCol).to.eql('NAME'); - //}); - - //it('should find escaped id column name', function() { - //var idCol = connector.idColumnEscaped('customer'); - //expect(idCol).to.eql('`NAME`'); - //}); - - //it('should find escaped table name', function() { - //var table = connector.tableEscaped('customer'); - //expect(table).to.eql('`CUSTOMER`'); - //}); - - //it('should find escaped column name', function() { - //var column = connector.columnEscaped('customer', 'vip'); - //expect(column).to.eql('`VIP`'); - //}); - - //it('should convert to escaped id column value', function() { - //var column = connector.idColumnValue('customer', 'John'); - //expect(column).to.eql('John'); - //}); - - //it('builds where', function() { - //var where = connector.buildWhere('customer', {name: 'John'}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME`=?', - //params: ['John'] - //}); - //}); - - //it('builds where with null', function() { - //var where = connector.buildWhere('customer', {name: null}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` IS NULL', - //params: [] - //}); - //}); - - //it('builds where with inq', function() { - //var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` IN (?,?)', - //params: ['John', 'Mary'] - //}); - //}); - - //it('builds where with or', function() { - //var where = connector.buildWhere('customer', - //{or: [{name: 'John'}, {name: 'Mary'}]}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE (`NAME`=?) OR (`NAME`=?)', - //params: ['John', 'Mary'] - //}); - //}); - - //it('builds where with and', function() { - //var where = connector.buildWhere('customer', - //{and: [{name: 'John'}, {vip: true}]}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE (`NAME`=?) AND (`VIP`=?)', - //params: ['John', true] - //}); - //}); - - //it('builds where with a regexp string that does not have flags', function() { - //var where = connector.buildWhere('customer', { - //name: { - //regexp: '^J' - //} - //}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` REGEXP ?', - //params: ['^J'] - //}); - //}); - - //it('builds where with a regexp string that has flags', function() { - //var where = connector.buildWhere('customer', { - //name: { - //regexp: '^J/i' - //} - //}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` REGEXP ?', - //params: ['^J/i'] - //}); - //}); - - //it('builds where with a regexp literal that does not have flags', function() { - //var where = connector.buildWhere('customer', { - //name: { - //regexp: /^J/ - //} - //}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` REGEXP ?', - //params: [/^J/] - //}); - //}); - - //it('builds where with a regexp literal that has flags', function() { - //var where = connector.buildWhere('customer', { - //name: { - //regexp: /^J/i - //} - //}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` REGEXP ?', - //params: [/^J/i] - //}); - //}); - - //it('builds where with a regexp object that does not have flags', function() { - //var where = connector.buildWhere('customer', { - //name: { - //regexp: new RegExp(/^J/) - //} - //}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` REGEXP ?', - //params: [/^J/] - //}); - //}); - - //it('builds where with a regexp object that has flags', function() { - //var where = connector.buildWhere('customer', { - //name: { - //regexp: new RegExp(/^J/i) - //} - //}); - //expect(where.toJSON()).to.eql({ - //sql: 'WHERE `NAME` REGEXP ?', - //params: [new RegExp(/^J/i)] - //}); - //}); - - //it('builds where with nesting and/or', 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))', - //params: ['John', true] - //}); - //}); - - //it('builds order by with one field', function() { - //var orderBy = connector.buildOrderBy('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 `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 `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(['`NAME`', '`VIP`']); - //expect(fields.columnValues[0].toJSON()).to.eql( - //{sql: '?', params: ['John']}); - //expect(fields.columnValues[1].toJSON()).to.eql( - //{sql: '?', params: [true]}); - //}); - - //it('builds fields for UPDATE without ids', function() { - //var fields = connector.buildFieldsForUpdate('customer', - //{name: 'John', vip: true}); - //expect(fields.toJSON()).to.eql({ - //sql: 'SET `VIP`=?', - //params: [true] - //}); - //}); - - //it('builds fields for UPDATE with ids', function() { - //var fields = connector.buildFieldsForUpdate('customer', - //{name: 'John', vip: true}, false); - //expect(fields.toJSON()).to.eql({ - //sql: 'SET `NAME`=?,`VIP`=?', - //params: ['John', true] - //}); - //}); - - //it('builds column names for SELECT', function() { - //var cols = connector.buildColumnNames('customer'); - //expect(cols).to.eql('`NAME`,`VIP`,`ADDRESS`'); - //}); - - //it('builds column names with true fields filter for SELECT', function() { - //var cols = connector.buildColumnNames('customer', {fields: {name: true}}); - //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('`VIP`,`ADDRESS`'); - //}); - - //it('builds column names with array fields filter for SELECT', function() { - //var cols = connector.buildColumnNames('customer', {fields: ['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 `NAME`=$1', - //params: ['John'] - //}); - //}); - - //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', - //params: [false, 'John'] - //}); - //}); - - //it('builds SELECT', function() { - //var sql = connector.buildSelect('customer', - //{order: 'name', limit: 5, where: {name: 'John'}}); - //expect(sql.toJSON()).to.eql({ - //sql: 'SELECT `NAME`,`VIP`,`ADDRESS` FROM `CUSTOMER`' + - //' WHERE `NAME`=$1 ORDER BY `NAME` LIMIT 5', - //params: ['John'] - //}); - //}); - - //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)', - //params: ['John', true] - //}); - //}); - - //it('normalizes a SQL statement from string', function() { - //var sql = 'SELECT * FROM `CUSTOMER`'; - //var stmt = new ParameterizedSQL(sql); - //expect(stmt.toJSON()).to.eql({sql: sql, params: []}); - //}); - - //it('normalizes a SQL statement from object without params', function() { - //var sql = {sql: 'SELECT * FROM `CUSTOMER`'}; - //var stmt = new ParameterizedSQL(sql); - //expect(stmt.toJSON()).to.eql({sql: sql.sql, params: []}); - //}); - - //it('normalizes a SQL statement from object with params', function() { - //var sql = - //{sql: 'SELECT * FROM `CUSTOMER` WHERE `NAME`=?', params: ['John']}; - //var stmt = new ParameterizedSQL(sql); - //expect(stmt.toJSON()).to.eql({sql: sql.sql, params: ['John']}); - //}); - - //it('should throw if the statement is not a string or object', function() { - //expect(function() { - //[>jshint unused:false <] - //var stmt = new ParameterizedSQL(true); - //}).to.throw('sql must be a string'); - //}); - - //it('concats SQL statements', function() { - //var stmt1 = {sql: 'SELECT * from `CUSTOMER`'}; - //var where = {sql: 'WHERE `NAME`=?', params: ['John']}; - //stmt1 = ParameterizedSQL.append(stmt1, where); - //expect(stmt1.toJSON()).to.eql( - //{sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); - //}); - - //it('concats string SQL statements', function() { - //var stmt1 = 'SELECT * from `CUSTOMER`'; - //var where = {sql: 'WHERE `NAME`=?', params: ['John']}; - //stmt1 = ParameterizedSQL.append(stmt1, where); - //expect(stmt1.toJSON()).to.eql( - //{sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); - //}); - - //it('should throw if params does not match placeholders', function() { - //expect(function() { - //var stmt1 = 'SELECT * from `CUSTOMER`'; - //var where = {sql: 'WHERE `NAME`=?', params: ['John', 'Mary']}; - //stmt1 = ParameterizedSQL.append(stmt1, where); - //}).to.throw('must match the number of params'); - //}); - - //it('should allow execute(sql, callback)', function(done) { - //connector.execute('SELECT * FROM `CUSTOMER`', done); - //}); - - //it('should allow execute(sql, params, callback)', function(done) { - //connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', - //['xyz'], done); - //}); - - //it('should allow execute(sql, params, options, callback)', function(done) { - //connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', - //['xyz'], {transaction: true}, done); - //}); - - //it('should throw if params is not an array for execute()', function() { - //expect(function() { - //connector.execute('SELECT * FROM `CUSTOMER`', 'xyz', function() { - //}); - //}).to.throw('params must be an array'); - //}); - - //it('should throw if options is not an object for execute()', function() { - //expect(function() { - //connector.execute('SELECT * FROM `CUSTOMER`', [], 'xyz', function() { - //}); - //}).to.throw('options must be an object'); - //}); - - //it('should throw if callback is not a function for execute()', function() { - //expect(function() { - //connector.execute('SELECT * FROM `CUSTOMER`', [], {}, 'xyz'); - //}).to.throw('callback must be a function'); - //}); - - //it('should invoke hooks', function(done) { - //var events = []; - //connector.observe('before execute', function(ctx, next) { - //expect(ctx.req.sql).be.a('string'); - //expect(ctx.req.params).be.a('array'); - //events.push('before execute'); - //next(); - //}); - //connector.observe('after execute', function(ctx, next) { - //expect(ctx.res).be.an('array'); - //events.push('after execute'); - //next(); - //}); - //Customer.find(function(err, results) { - //expect(events).to.eql(['before execute', 'after execute']); - //done(err, results); - //}); - //}); -//}); +var expect = require('chai').expect; +var SQLConnector = require('../lib/sql'); +var ParameterizedSQL = SQLConnector.ParameterizedSQL; +var testConnector = require('./connectors/test-sql-connector'); + +var juggler = require('loopback-datasource-juggler'); +var ds = new juggler.DataSource({ + connector: testConnector, + debug: true +}); +var connector; +var Customer; +var Order; +var Store; +var Retailer; + +describe('sql connector', function() { + before(function() { + connector = ds.connector; + connector._tables = {}; + connector._models = {}; + Customer = ds.createModel('customer', + { + name: { + id: true, + type: String, + testdb: { + column: 'NAME', + dataType: 'VARCHAR', + dataLength: 32 + } + }, + vip: { + type: Boolean, + testdb: { + column: 'VIP' + } + }, + address: String + }, + {testdb: {table: 'CUSTOMER'}} + ); + + Order = ds.createModel('order', + { + id: { + id: true + }, + date: Date + } + ); + + Store = ds.createModel('store', + { + id: { + id: true, + type: String + }, + state: String + } + ); + + Retailer = ds.createModel('retailer', + { + id: { + id: true, + type: String + }, + name: String + } + ); + + // Relations + Customer.hasMany(Order, {as: 'orders', foreignKey: 'customer_id'}); + Order.belongsTo(Customer, {as: 'customer', foreignKey: 'customer_id'}); + Order.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); + Store.hasMany(Order, {as: 'orders', foreignKey: 'store_id'}); + Store.hasMany(Customer, {as: 'customers', through: Order, foreignKey: 'store_id', + keyThrough: 'customer_id'}); + Customer.belongsTo(Store, {as: 'store', foreignKey: 'store_id'}); + Store.hasMany(Customer, {as: 'customers', foreignKey: 'store_id'}); + Store.belongsTo(Retailer, {as: 'retailer', foreignKey: 'retailer_id'}); + }); + + it('should map table name', function() { + var table = connector.table('customer'); + expect(table).to.eql('CUSTOMER'); + }); + + it('should map column name', function() { + var column = connector.column('customer', 'name'); + expect(column).to.eql('NAME'); + }); + + it('should find column metadata', function() { + var column = connector.columnMetadata('customer', 'name'); + expect(column).to.eql({ + column: 'NAME', + dataType: 'VARCHAR', + dataLength: 32 + }); + }); + + it('should map property name', function() { + var prop = connector.propertyName('customer', 'NAME'); + expect(prop).to.eql('name'); + }); + + it('should map id column name', function() { + var idCol = connector.idColumn('customer'); + expect(idCol).to.eql('NAME'); + }); + + it('should find escaped id column name', function() { + var idCol = connector.idColumnEscaped('customer'); + expect(idCol).to.eql('`NAME`'); + }); + + it('should find escaped table name', function() { + var table = connector.tableEscaped('customer'); + expect(table).to.eql('`CUSTOMER`'); + }); + + it('should find escaped column name', function() { + var column = connector.columnEscaped('customer', 'vip'); + expect(column).to.eql('`VIP`'); + }); + + it('should find qualified column name', function() { + var column = connector.columnEscaped('customer', 'vip', true); + expect(column).to.eql('`CUSTOMER`.`VIP`'); + }); + + it('should convert to escaped id column value', function() { + var column = connector.idColumnValue('customer', 'John'); + expect(column).to.eql('John'); + }); + + it('builds where', function() { + var where = connector.buildWhere('customer', {name: 'John'}); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `NAME`=?', + params: ['John'] + }); + }); + + it('builds where', function() { + var where = connector.buildWhere('order', {customer: {name: 'John'}}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME`=?', + params: ['John'] + }); + }); + + it('builds qualified where', function() { + var where = connector.buildWhere('customer', {name: 'John'}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME`=?', + params: ['John'] + }); + }); + + it('builds where with null', function() { + var where = connector.buildWhere('customer', {name: null}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` IS NULL', + params: [] + }); + }); + + it('builds where with inq', function() { + var where = connector.buildWhere('customer', {name: {inq: ['John', 'Mary']}}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` IN (?,?)', + params: ['John', 'Mary'] + }); + }); + + it('builds where with or', function() { + var where = connector.buildWhere('customer', + {or: [{name: 'John'}, {name: 'Mary'}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`CUSTOMER`.`NAME`=?)', + params: ['John', 'Mary'] + }); + }); + + it('builds where with or and relation', function() { + var where = connector.buildWhere('customer', + {or: [{name: 'John'}, {store: {state: 'NY'}}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) OR (`STORE`.`STATE`=?)', + params: ['John', 'NY'] + }); + }); + + it('builds where with and', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {vip: true}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`CUSTOMER`.`VIP`=?)', + params: ['John', true] + }); + }); + + it('builds where with and + relation', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {store: {state: 'NY'}}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`STORE`.`STATE`=?)', + params: ['John', 'NY'] + }); + }); + + it('builds where with a regexp string that does not have flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: '^J' + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: ['^J'] + }); + }); + + it('builds where with a regexp string that has flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: '^J/i' + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: ['^J/i'] + }); + }); + + it('builds where with a regexp literal that does not have flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: /^J/ + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [/^J/] + }); + }); + + it('builds where with a regexp literal that has flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: /^J/i + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [/^J/i] + }); + }); + + it('builds where with a regexp object that does not have flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: new RegExp(/^J/) + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [/^J/] + }); + }); + + it('builds where with a regexp object that has flags', function() { + var where = connector.buildWhere('customer', { + name: { + regexp: new RegExp(/^J/i) + } + }, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `CUSTOMER`.`NAME` REGEXP ?', + params: [new RegExp(/^J/i)] + }); + }); + + it('builds where with nesting and/or', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {or: [{vip: true}, {address: null}]}]}, true); + expect(where.toJSON()).to.eql({ + 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 `CUSTOMER`.`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`'); + }); + + 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'); + }); + + it('builds order by with relation field', function() { + var orderBy = connector.buildOrderBy('customer', {'store': 'state'}); + expect(orderBy).to.eql('ORDER BY `STORE`.`STATE`'); + }); + + it('builds order by with multiple relation fields', function() { + var orderBy = connector.buildOrderBy('customer', {'store': ['state', 'id']}); + expect(orderBy).to.eql('ORDER BY `STORE`.`STATE`,`STORE`.`ID`'); + }); + + it('builds order by with deep relation field', function() { + var orderBy = connector.buildOrderBy('customer', {'store': {'retailer': 'name'}}); + expect(orderBy).to.eql('ORDER BY `RETAILER`.`NAME`'); + }); + + it('builds fields for columns', function() { + var fields = connector.buildFields('customer', + {name: 'John', vip: true, unknown: 'Random'}, false, true); + 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( + {sql: '?', params: [true]}); + }); + + it('builds fields for UPDATE without ids', function() { + var fields = connector.buildFieldsForUpdate('customer', + {name: 'John', vip: true}); + expect(fields.toJSON()).to.eql({ + sql: 'SET `CUSTOMER`.`VIP`=?', + params: [true] + }); + }); + + it('builds fields for UPDATE with ids', function() { + var fields = connector.buildFieldsForUpdate('customer', + {name: 'John', vip: true}, false); + expect(fields.toJSON()).to.eql({ + sql: 'SET `CUSTOMER`.`NAME`=?,`CUSTOMER`.`VIP`=?', + params: ['John', true] + }); + }); + + it('builds column names for SELECT', function() { + var cols = connector.buildColumnNames('customer', {}, true); + expect(cols).to + .eql('`CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`STORE_ID`'); + }); + + it('builds column names with true fields filter for SELECT', function() { + var cols = connector.buildColumnNames('customer', {fields: {name: true}}, true); + 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}}, true); + expect(cols).to.eql('`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,`CUSTOMER`.`STORE_ID`'); + }); + + it('builds column names with array fields filter for SELECT', function() { + var cols = connector.buildColumnNames('customer', {fields: ['name']}, true); + 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 `CUSTOMER`.`NAME`=$1', + params: ['John'] + }); + }); + + 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', + params: [false, 'John'] + }); + }); + + it('builds SELECT', 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`.`STORE_ID` FROM `CUSTOMER`' + + ' WHERE `CUSTOMER`.`NAME`=$1 ORDER BY `CUSTOMER`.`NAME` LIMIT 5', + params: ['John'] + }); + }); + + it('builds SELECT with inner join', function() { + var sql = connector.buildSelect('customer', + {order: 'name', limit: 5, where: {store: {state: 'NY'}}}); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + + ' INNER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' WHERE `STORE`.`STATE`=$1 ORDER BY `CUSTOMER`.`NAME` LIMIT 5', + params: ['NY'] + }); + }); + + it('builds SELECT with left outer join', function() { + var sql = connector.buildSelect('customer', + {order: {store: 'state'}, limit: 5}); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + + ' LEFT OUTER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' ORDER BY `STORE`.`STATE` LIMIT 5', + params: [] + }); + }); + + it('builds SELECT with left inner join because on search by relation', function() { + var sql = connector.buildSelect('customer', + {where: {store: {state: 'NY'}}, order: {store: 'state'}, limit: 5}); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + + ' INNER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' WHERE `STORE`.`STATE`=$1 ORDER BY `STORE`.`STATE` LIMIT 5', + params: ['NY'] + }); + }); + + it('builds SELECT with left inner and outer joins because', function() { + var sql = connector.buildSelect('customer', + {where: {store: {state: 'NY'}}, order: {store: {retailer: 'name'}}, limit: 5}); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + + ' INNER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' LEFT OUTER JOIN `RETAILER` ON `STORE`.`RETAILER_ID`=`RETAILER`.`ID`' + + ' WHERE `STORE`.`STATE`=$1 ORDER BY `RETAILER`.`NAME` LIMIT 5', + params: ['NY'] + }); + }); + + it('builds SELECT with deep inner join', function() { + var sql = connector.buildSelect('customer', + {order: 'name', limit: 5, where: {store: {retailer: {name: 'oxxo'}}}}); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + + ' INNER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' INNER JOIN `RETAILER` ON `STORE`.`RETAILER_ID`=`RETAILER`.`ID`' + + ' WHERE `RETAILER`.`NAME`=$1 ORDER BY `CUSTOMER`.`NAME` LIMIT 5', + params: ['oxxo'] + }); + }); + + 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)', + params: ['John', true] + }); + }); + + it('normalizes a SQL statement from string', function() { + var sql = 'SELECT * FROM `CUSTOMER`'; + var stmt = new ParameterizedSQL(sql); + expect(stmt.toJSON()).to.eql({sql: sql, params: []}); + }); + + it('normalizes a SQL statement from object without params', function() { + var sql = {sql: 'SELECT * FROM `CUSTOMER`'}; + var stmt = new ParameterizedSQL(sql); + expect(stmt.toJSON()).to.eql({sql: sql.sql, params: []}); + }); + + it('normalizes a SQL statement from object with params', function() { + var sql = + {sql: 'SELECT * FROM `CUSTOMER` WHERE `NAME`=?', params: ['John']}; + var stmt = new ParameterizedSQL(sql); + expect(stmt.toJSON()).to.eql({sql: sql.sql, params: ['John']}); + }); + + it('should throw if the statement is not a string or object', function() { + expect(function() { + /*jshint unused:false */ + var stmt = new ParameterizedSQL(true); + }).to.throw('sql must be a string'); + }); + + it('concats SQL statements', function() { + var stmt1 = {sql: 'SELECT * from `CUSTOMER`'}; + var where = {sql: 'WHERE `NAME`=?', params: ['John']}; + stmt1 = ParameterizedSQL.append(stmt1, where); + expect(stmt1.toJSON()).to.eql( + {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); + }); + + it('concats string SQL statements', function() { + var stmt1 = 'SELECT * from `CUSTOMER`'; + var where = {sql: 'WHERE `NAME`=?', params: ['John']}; + stmt1 = ParameterizedSQL.append(stmt1, where); + expect(stmt1.toJSON()).to.eql( + {sql: 'SELECT * from `CUSTOMER` WHERE `NAME`=?', params: ['John']}); + }); + + it('should throw if params does not match placeholders', function() { + expect(function() { + var stmt1 = 'SELECT * from `CUSTOMER`'; + var where = {sql: 'WHERE `NAME`=?', params: ['John', 'Mary']}; + stmt1 = ParameterizedSQL.append(stmt1, where); + }).to.throw('must match the number of params'); + }); + + it('should allow execute(sql, callback)', function(done) { + connector.execute('SELECT * FROM `CUSTOMER`', done); + }); + + it('should allow execute(sql, params, callback)', function(done) { + connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', + ['xyz'], done); + }); + + it('should allow execute(sql, params, options, callback)', function(done) { + connector.execute('SELECT * FROM `CUSTOMER` WHERE `NAME`=$1', + ['xyz'], {transaction: true}, done); + }); + + it('should throw if params is not an array for execute()', function() { + expect(function() { + connector.execute('SELECT * FROM `CUSTOMER`', 'xyz', function() { + }); + }).to.throw('params must be an array'); + }); + + it('should throw if options is not an object for execute()', function() { + expect(function() { + connector.execute('SELECT * FROM `CUSTOMER`', [], 'xyz', function() { + }); + }).to.throw('options must be an object'); + }); + + it('should throw if callback is not a function for execute()', function() { + expect(function() { + connector.execute('SELECT * FROM `CUSTOMER`', [], {}, 'xyz'); + }).to.throw('callback must be a function'); + }); + + it('should invoke hooks', function(done) { + var events = []; + connector.observe('before execute', function(ctx, next) { + expect(ctx.req.sql).be.a('string'); + expect(ctx.req.params).be.a('array'); + events.push('before execute'); + next(); + }); + connector.observe('after execute', function(ctx, next) { + expect(ctx.res).be.an('array'); + events.push('after execute'); + next(); + }); + Customer.find(function(err, results) { + expect(events).to.eql(['before execute', 'after execute']); + done(err, results); + }); + }); +}); From c23ddeacd89bec92386288e6a849dbde500563cc Mon Sep 17 00:00:00 2001 From: kolach Date: Tue, 22 Mar 2016 21:23:49 -0600 Subject: [PATCH 06/12] bugfix --- lib/sql.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/sql.js b/lib/sql.js index 8c65ba10..90349545 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -861,7 +861,9 @@ SQLConnector.prototype._buildWhere = function(model, where, qualified) { var stmt = new ParameterizedSQL('', []); // Handle nested relation if (relations && key in relations) { - var stmtForRelation = self._buildWhere(key, where[key], qualified); + var rel = relations[key]; + var modelTo = rel.modelTo.definition.name; + var stmtForRelation = self._buildWhere(modelTo, where[key], qualified); whereStmts.push(stmtForRelation); continue; } From a64db67f20dec4f61ce38c7c155386fb363c932a Mon Sep 17 00:00:00 2001 From: kolach Date: Tue, 22 Mar 2016 21:30:42 -0600 Subject: [PATCH 07/12] update table without qualified columns --- lib/sql.js | 4 ++-- test/sql.test.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index 90349545..50c1bee6 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -610,7 +610,7 @@ SQLConnector.prototype.updateAttributes = function(model, id, data, options, cb) SQLConnector.prototype.buildUpdate = function(model, where, data, options) { var fields = this.buildFieldsForUpdate(model, data); var updateClause = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model)); - var whereClause = this.buildWhere(model, where, true); + var whereClause = this.buildWhere(model, where); updateClause.merge([fields, whereClause]); return this.parameterize(updateClause); }; @@ -1075,7 +1075,7 @@ SQLConnector.prototype.buildFieldsForUpdate = function(model, data, excludeIds) if (excludeIds === undefined) { excludeIds = true; } - var fields = this.buildFields(model, data, excludeIds, true); + var fields = this.buildFields(model, data, excludeIds); var columns = new ParameterizedSQL(''); for (var i = 0, n = fields.names.length; i < n; i++) { var clause = ParameterizedSQL.append(fields.names[i], diff --git a/test/sql.test.js b/test/sql.test.js index f858c2db..7f800a50 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -337,7 +337,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] }); }); @@ -346,7 +346,7 @@ 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] }); }); @@ -383,7 +383,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'] }); }); From 03c3ffd31d2a43c2579d68256572146163de786d Mon Sep 17 00:00:00 2001 From: kolach Date: Wed, 23 Mar 2016 00:16:58 -0600 Subject: [PATCH 08/12] compact models --- test/sql.test.js | 83 ++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 49 deletions(-) diff --git a/test/sql.test.js b/test/sql.test.js index 7f800a50..a6d24670 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -19,56 +19,41 @@ describe('sql connector', function() { connector = ds.connector; connector._tables = {}; connector._models = {}; - Customer = ds.createModel('customer', - { - name: { - id: true, - type: String, - testdb: { - column: 'NAME', - dataType: 'VARCHAR', - dataLength: 32 - } - }, - vip: { - type: Boolean, - testdb: { - column: 'VIP' - } - }, - address: String + Customer = ds.createModel('customer', { + name: { + id: true, + type: String, + testdb: { + column: 'NAME', + dataType: 'VARCHAR', + dataLength: 32 + } }, - {testdb: {table: 'CUSTOMER'}} - ); - - Order = ds.createModel('order', - { - id: { - id: true - }, - date: Date - } - ); - - Store = ds.createModel('store', - { - id: { - id: true, - type: String - }, - state: String - } - ); - - Retailer = ds.createModel('retailer', - { - id: { - id: true, - type: String - }, - name: String - } - ); + vip: { + type: Boolean, + testdb: { + column: 'VIP' + } + }, + address: String + }, { + testdb: {table: 'CUSTOMER'} + }); + + Order = ds.createModel('order', { + id: {id: true, type: String}, + date: Date + }); + + Store = ds.createModel('store', { + id: {id: true, type: String}, + state: String + }); + + Retailer = ds.createModel('retailer', { + id: {id: true, type: String}, + name: String + }); // Relations Customer.hasMany(Order, {as: 'orders', foreignKey: 'customer_id'}); From e2bc582a0a9d09e8e761f5a7059aae23966d0094 Mon Sep 17 00:00:00 2001 From: kolach Date: Wed, 23 Mar 2016 09:59:13 -0600 Subject: [PATCH 09/12] sync with develop branch --- lib/sql.js | 113 +++-------------------------------------------- lib/utils.js | 18 -------- test/sql.test.js | 1 - 3 files changed, 5 insertions(+), 127 deletions(-) delete mode 100644 lib/utils.js diff --git a/lib/sql.js b/lib/sql.js index 36fecc25..50c1bee6 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; @@ -413,6 +412,10 @@ SQLConnector.prototype.execute = function(sql, params, options, callback) { }; this.notifyObserversAround('execute', context, function(context, done) { self.executeSQL(context.req.sql, context.req.params, context.options, function(err, info) { + if(err){ + debug('Error: %j %j %j', err, context.req.sql, context.req.params); + } + if (!err && info != null) { context.res = info; } @@ -862,6 +865,7 @@ SQLConnector.prototype._buildWhere = function(model, where, qualified) { var modelTo = rel.modelTo.definition.name; var stmtForRelation = self._buildWhere(modelTo, where[key], qualified); whereStmts.push(stmtForRelation); + continue; } // Handle and/or operators if (key === 'and' || key === 'or') { @@ -1137,8 +1141,6 @@ SQLConnector.prototype.buildColumnNames = function(model, filter, qualified) { * @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]} */ SQLConnector.prototype.buildSelect = function(model, filter, options) { - options = options || {}; - if (!filter.order) { var idNames = this.idNames(model); if (idNames && idNames.length) { @@ -1175,75 +1177,9 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { } } - - if (options.skipParameterize === true) { - return selectStmt; - } - return this.parameterize(selectStmt); }; -/** - * Build the SQL INNER JOIN clauses - * @param {string} model Model name - * @param {object} where An object for the where conditions - * @returns {ParameterizedSQL} The SQL INNER JOIN clauses - */ -SQLConnector.prototype.buildJoins = function(model, where) { - var modelDef = this.getModelDefinition(model); - var relations = modelDef.model.relations; - var stmt = new ParameterizedSQL('', []); - - var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) { - var modelToAlias = this.escapeName(this.table(modelTo)); - var innerFilter = assign({}, filter); - var innerIdField = {}; - innerIdField[keyTo] = true; - innerFilter.fields = assign({}, innerFilter.fields, innerIdField); - - var condition = this.columnEscaped(modelFrom, keyFrom) + '=' + - modelToAlias + '.' + this.escapeName(this.column(modelTo, keyTo)); - - var innerSelect = this.buildSelect(modelTo, innerFilter, { - skipParameterize: true - }); - - return new ParameterizedSQL('INNER JOIN (', []) - .merge(innerSelect) - .merge(') AS ' + modelToAlias) - .merge('ON ' + condition); - }.bind(this); - - for (var key in where) { - if (!(key in relations)) continue; - - var rel = relations[key]; - var keyFrom = rel.keyFrom; - var modelTo = rel.modelTo.definition.name; - var keyTo = rel.keyTo; - - var join; - if (!rel.modelThrough) { - // 1:n relation - join = buildOneToMany(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._ids[0].name; - var innerFilter = {fields: {}}; - innerFilter.fields[keyThrough] = true; - - var joinInner = buildOneToMany(model, keyFrom, modelThrough, keyTo, innerFilter); - join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key]); - join = joinInner.merge(join); - } - stmt.merge(join); - } - - return stmt; -}; - /** * Transform the row data into a model data object * @param {string} model Model name @@ -1345,45 +1281,6 @@ SQLConnector.prototype.find = function(model, id, options, cb) { // Alias to `find`. Juggler checks `findById` only. Connector.defineAliases(SQLConnector.prototype, 'find', ['findById']); -/** - * Build a SQL SELECT statement to count rows - * @param {String} model Model name - * @param {Object} where Where object - * @param {Object} options Options object - * @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 count = 'count(*)'; - if (haveRelationFilters) { - var idColumn = this.columnEscaped(model, this.idColumn(model)); - count = 'count(DISTINCT ' + idColumn + ')'; - } - - var stmt = new ParameterizedSQL('SELECT ' + count + - ' as "cnt" FROM ' + this.tableEscaped(model)); - - if (haveRelationFilters) { - var joinsStmts = this.buildJoins(model, where); - stmt = stmt.merge(joinsStmts); - } - - stmt = stmt.merge(this.buildWhere(model, where)); - return this.parameterize(stmt); -}; - /** * Count all model instances by the where filter * 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 c1205102..a6d24670 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -19,7 +19,6 @@ describe('sql connector', function() { connector = ds.connector; connector._tables = {}; connector._models = {}; - Customer = ds.createModel('customer', { name: { id: true, From 11ace3f7833ef3d7a884b5272a3f68a862446cf0 Mon Sep 17 00:00:00 2001 From: kolach Date: Sat, 26 Mar 2016 00:33:05 -0600 Subject: [PATCH 10/12] support joining for AND and OR conditions --- lib/sql.js | 44 +++++++++++++++++++++++++++----------------- test/sql.test.js | 26 ++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index 50c1bee6..77204e94 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -660,17 +660,30 @@ SQLConnector.prototype.buildWhere = function(model, where, qualified) { * @param {object|array|string} order order by clouse * @returns {array} Array of relations to make joins */ -SQLConnector.prototype._innerRelations = function(model, where) { - var result = []; +SQLConnector.prototype._innerRelations = function(model, where, a) { + var result = a || []; var relations = this.getModelDefinition(model).model.relations; - if (relations) { - for (var key in where) { - if (key in relations) { - var rel = relations[key]; - var modelTo = rel.modelTo.definition.name; - result.push(rel); - result = result.concat(this._innerRelations(modelTo, where[key])); - } + + for (var key in where) { + switch (key) { + // key can be a condition operator `or` or `and` + case 'and': + case 'or': + var cond = where[key]; + if (cond.constructor === Array) { + for (var i = 0, l = cond.length; i < l; i++) { + this._innerRelations(model, cond[i], result); + } + } + break; + + default: + if (relations && key in relations) { + var rel = relations[key]; + var modelTo = rel.modelTo.definition.name; + if (result.indexOf(rel) === -1) { result.push(rel); } + this._innerRelations(modelTo, where[key], result); + } } } return result; @@ -682,9 +695,8 @@ SQLConnector.prototype._innerRelations = function(model, where) { * @param {object|array|string} order order by clouse * @returns {array} Array of relations */ -SQLConnector.prototype._outerRelations = function(model, orders) { - var result = []; - +SQLConnector.prototype._outerRelations = function(model, orders, a) { + var result = a || []; var self = this; var relations = this.getModelDefinition(model).model.relations; @@ -699,9 +711,9 @@ SQLConnector.prototype._outerRelations = function(model, orders) { for (var key in order) { if (key in relations) { var rel = relations[key]; - result.push(rel); var modelTo = rel.modelTo.definition.name; - result = result.concat(self._outerRelations(modelTo, order[key])); + if (result.indexOf(rel) === -1) { result.push(rel); } + self._outerRelations(modelTo, order[key], result); } } } @@ -711,8 +723,6 @@ SQLConnector.prototype._outerRelations = function(model, orders) { }; - - /** * Build the SQL JOIN clauses for planed joins array * @param {string} model Model name diff --git a/test/sql.test.js b/test/sql.test.js index a6d24670..c5713f6b 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -47,6 +47,7 @@ describe('sql connector', function() { Store = ds.createModel('store', { id: {id: true, type: String}, + name: String, state: String }); @@ -197,6 +198,15 @@ describe('sql connector', function() { }); }); + it('builds where with and + 2 same relations', function() { + var where = connector.buildWhere('customer', + {and: [{name: 'John'}, {store: {state: 'NY'}}, {store: {name: 'oxxo'}}]}, true); + expect(where.toJSON()).to.eql({ + sql: 'WHERE (`CUSTOMER`.`NAME`=?) AND (`STORE`.`STATE`=?) AND (`STORE`.`NAME`=?)', + params: ['John', 'NY', 'oxxo'] + }); + }); + it('builds where with a regexp string that does not have flags', function() { var where = connector.buildWhere('customer', { name: { @@ -408,7 +418,7 @@ describe('sql connector', function() { }); }); - it('builds SELECT with left inner join because on search by relation', function() { + it('builds SELECT with left inner join', function() { var sql = connector.buildSelect('customer', {where: {store: {state: 'NY'}}, order: {store: 'state'}, limit: 5}); expect(sql.toJSON()).to.eql({ @@ -420,7 +430,19 @@ describe('sql connector', function() { }); }); - it('builds SELECT with left inner and outer joins because', function() { + it('builds SELECT with 1 left inner join even if relation is met 2 times', function() { + var sql = connector.buildSelect('customer', + {where: {and: [{store: {state: 'NY'}}, {store: {name: 'oxxo'}}]}, order: {store: 'state'}, limit: 5}); + expect(sql.toJSON()).to.eql({ + sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + + ' INNER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' WHERE (`STORE`.`STATE`=$1) AND (`STORE`.`NAME`=$2) ORDER BY `STORE`.`STATE` LIMIT 5', + params: ['NY', 'oxxo'] + }); + }); + + it('builds SELECT with left inner and outer joins', function() { var sql = connector.buildSelect('customer', {where: {store: {state: 'NY'}}, order: {store: {retailer: 'name'}}, limit: 5}); expect(sql.toJSON()).to.eql({ From 28546b6c01efeac53a83cbcc3c8461fef9a79588 Mon Sep 17 00:00:00 2001 From: kolach Date: Sat, 26 Mar 2016 19:24:57 -0600 Subject: [PATCH 11/12] LEFT OUTER JOIN == LEFT JOIN --- lib/sql.js | 4 ++-- test/sql.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index 77204e94..2c8df241 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -668,7 +668,7 @@ SQLConnector.prototype._innerRelations = function(model, where, a) { switch (key) { // key can be a condition operator `or` or `and` case 'and': - case 'or': + case 'or': var cond = where[key]; if (cond.constructor === Array) { for (var i = 0, l = cond.length; i < l; i++) { @@ -735,7 +735,7 @@ SQLConnector.prototype.buildJoins = function(model, where, order) { var self = this; function makeJoinStmt(isInner, rel) { - var joinType = isInner ? 'INNER JOIN ' : 'LEFT OUTER JOIN '; + var joinType = isInner ? 'INNER JOIN ' : 'LEFT JOIN '; var modelFrom = rel.modelFrom.definition.name; var modelTo = rel.modelTo.definition.name; var keyFrom = rel.keyFrom; diff --git a/test/sql.test.js b/test/sql.test.js index c5713f6b..1d80cfb0 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -412,7 +412,7 @@ describe('sql connector', function() { expect(sql.toJSON()).to.eql({ sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + - ' LEFT OUTER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + + ' LEFT JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + ' ORDER BY `STORE`.`STATE` LIMIT 5', params: [] }); @@ -449,7 +449,7 @@ describe('sql connector', function() { sql: 'SELECT `CUSTOMER`.`NAME`,`CUSTOMER`.`VIP`,`CUSTOMER`.`ADDRESS`,' + '`CUSTOMER`.`STORE_ID` FROM `CUSTOMER`' + ' INNER JOIN `STORE` ON `CUSTOMER`.`STORE_ID`=`STORE`.`ID`' + - ' LEFT OUTER JOIN `RETAILER` ON `STORE`.`RETAILER_ID`=`RETAILER`.`ID`' + + ' LEFT JOIN `RETAILER` ON `STORE`.`RETAILER_ID`=`RETAILER`.`ID`' + ' WHERE `STORE`.`STATE`=$1 ORDER BY `RETAILER`.`NAME` LIMIT 5', params: ['NY'] }); From a883faf72d3cea866eff782dccfe44f2db540229 Mon Sep 17 00:00:00 2001 From: kolach Date: Sat, 26 Mar 2016 19:38:13 -0600 Subject: [PATCH 12/12] comments --- lib/sql.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/sql.js b/lib/sql.js index 2c8df241..73d0088c 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -657,8 +657,9 @@ SQLConnector.prototype.buildWhere = function(model, where, qualified) { /** * Build the inner joins for the where object * @param {string} model Model name - * @param {object|array|string} order order by clouse - * @returns {array} Array of relations to make joins + * @param {object} where where clouse + * @param {array} a array to push a relation + * @returns {array} Array of relations to make inner joins */ SQLConnector.prototype._innerRelations = function(model, where, a) { var result = a || []; @@ -690,10 +691,11 @@ SQLConnector.prototype._innerRelations = function(model, where, a) { }; /** - * Build the outer joins for the where object + * Build the left [outer] joins for the order object * @param {string} model Model name * @param {object|array|string} order order by clouse - * @returns {array} Array of relations + * @param {array} a array to store outer relations + * @returns {array} Array of left outer relations */ SQLConnector.prototype._outerRelations = function(model, orders, a) { var result = a || [];