Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 154 additions & 13 deletions lib/sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
};

/*!
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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(',');
Expand Down Expand Up @@ -987,18 +1004,39 @@ 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) {
filter.order = idNames;
}
}

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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
*
Expand All @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -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;
Loading