Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query relation #44

Closed
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
240 changes: 188 additions & 52 deletions lib/sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -229,24 +228,15 @@ SQLConnector.prototype.tableEscaped = function(model) {
* @param {String} property The property name
* @returns {String} The escaped column name
*/
SQLConnector.prototype.columnEscaped = function(model, property) {
return this.escapeName(this.column(model, property));
};

/**
* Get the escaped qualified column name (table.column for join)
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The escaped column name
*/
SQLConnector.prototype.qualifiedColumnEscaped = function(model, property) {
var table = this.tableEscaped(model);
var index = table.indexOf('.');
if (index !== -1) {
// Remove the schema name
table = table.substring(index);
SQLConnector.prototype.columnEscaped = function(model, property, relName) {
var prefix = '';
if (!relName) {
prefix = this.tableEscaped(model);
} else {
prefix = this.escapeName(relName);
}
return table + '.' + this.escapeName(this.column(model, property));

return prefix + '.' + this.escapeName(this.column(model, property));
};

/*!
Expand Down Expand Up @@ -749,7 +739,7 @@ SQLConnector.prototype.buildExpression = function(columnName, operator, columnVa
* @returns {ParameterizedSQL}
* @private
*/
SQLConnector.prototype._buildWhere = function(model, where) {
SQLConnector.prototype._buildWhere = function(model, where, asName) {
if (!where) {
return new ParameterizedSQL('');
}
Expand All @@ -776,10 +766,64 @@ SQLConnector.prototype._buildWhere = function(model, where) {
var clauses = where[key];
if (Array.isArray(clauses)) {
for (var i = 0, n = clauses.length; i < n; i++) {
var relationClauses = [];
for (var ck in clauses[i]) {
if (ck in relations) {
// Build a where clause with relation model
var relClauses = [];
var rel = relations[ck];
var relModel = rel.modelTo;
var relWhere = clauses[i][ck];
var lastRelWhere = relWhere;
var loopRelName = '';

do {
lastRelWhere = relWhere;
if (loopRelName) {
loopRelName = loopRelName + '_' + rel.name;
} else {
loopRelName = rel.name;
}
var loopRelModel = relModel;

for (var wk in lastRelWhere) {
if (wk in relModel.relations) {
rel = relModel.relations[wk];
relModel = rel.modelTo;
relWhere = lastRelWhere[wk];
} else {
var w = {};
w[wk] = lastRelWhere[wk];
relClauses.push(self._buildWhere(loopRelModel.definition.name, w, loopRelName));
}
}
} while(lastRelWhere !== relWhere);

if (relClauses.length) {
relationClauses = relationClauses.concat(relClauses);
}
}
}

var stmtForClause = self._buildWhere(model, clauses[i]);
stmtForClause.sql = '(' + stmtForClause.sql + ')';
branchParams = branchParams.concat(stmtForClause.params);
branches.push(stmtForClause.sql);

if (relationClauses.length) {
var relationSql = [];

for (var y = 0; y < relationClauses.length; y++) {
branchParams = branchParams.concat(relationClauses[y].params);
relationSql.push(relationClauses[y].sql);
}

if (stmtForClause.sql.length) {
stmtForClause.sql = stmtForClause.sql + ' AND ' + relationSql.join(' AND ');
} else {
stmtForClause.sql = relationSql.join(' AND ');
}
}

branches.push('(' + stmtForClause.sql + ')');
}
stmt.merge({
sql: branches.join(' ' + key.toUpperCase() + ' '),
Expand All @@ -790,7 +834,7 @@ SQLConnector.prototype._buildWhere = function(model, where) {
}
// The value is not an array, fall back to regular fields
}
var columnName = self.columnEscaped(model, key);
var columnName = self.columnEscaped(model, key, asName);
var expression = where[key];
var columnValue;
var sqlExp;
Expand Down Expand Up @@ -889,12 +933,25 @@ SQLConnector.prototype.buildOrderBy = function(model, order) {
} else {
// Column name is in the format: relationName.columnName
var colSplit = t[0].split('.');
// Find the name of the relation's model ...
var modelDef = this.getModelDefinition(model);
var relation = modelDef.model.relations[colSplit[0]];
var colModel = relation.modelTo.definition.name;
// ... and escape them
colName = self.columnEscaped(colModel, colSplit[1]);
var prevModel = model;
var relName = '';
for (var y = 0; y < colSplit.length; y++) {
if (y >= colSplit.length - 1) {
break;
}
// Find the name of the relation's model ...
var modelDef = this.getModelDefinition(prevModel);
var relation = modelDef.model.relations[colSplit[y]];
var colModel = relation.modelTo.definition.name;
prevModel = colModel;
if (relName) {
relName = relName + '_' + relation.name;
} else {
relName = relation.name;
}
// ... and escape them
colName = self.columnEscaped(colModel, colSplit[colSplit.length - 1], relName);
}
}

if (t.length === 1) {
Expand Down Expand Up @@ -1017,23 +1074,82 @@ SQLConnector.prototype.buildColumnNames = function(model, filter) {
};

/**
* Build a SQL SELECT statement
* Prepare filter data form buildSelect and buildCount
* @param {String} model Model name
* @param {Object} filter Filter object
* @param {Object} options Options object
* @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]}
* @returns {Object} filter Filter object
*/
SQLConnector.prototype.buildSelect = function(model, filter, options) {
options = options || {};
SQLConnector.prototype._prepareFilter = function (model, filter) {
var relations = this.getModelDefinition(model).model.relations;

if (!filter.order) {
var idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
} else {
var order = filter.order;
if (!Array.isArray(order)) {
order = [ order ];
}

order.forEach(function (order) {
var nextWhere = filter.where = filter.where || {};
var col = order.split(/[\s,]+/)[0].split('.');
if (col.length <= 1) {
return;
}

for (var i = 0; i < col.length; i++) {
if (i >= col.length -1) {
break;
}
nextWhere = nextWhere[col[i]] = nextWhere[col[i]] || {};
}
});
}

if (filter.where) {
fixWhere(filter.where, relations, filter.where);
}

return filter;

// Fix and/or relations
// Make sure if a relation is targetted in a `and`/`or`
// we create a empty where clause that will produce a join later
function fixWhere (where, relations, dest) {
if (Array.isArray(where)) {
where.forEach(function (where) {
fixWhere(where, relations, dest);
});
} else {
for (var key in where) {
if (key === 'and' || key === 'or') {
fixWhere(where[key], relations, dest);
} else if (key in relations) {
dest[key] = dest[key] || {};
fixWhere(where[key], relations[key].modelTo.relations, dest[key]);
}
}
}
}
};

/**
* Build a SQL SELECT statement
* @param {String} model Model name
* @param {Object} filter Filter object
* @param {Object} options Options object
* @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]}
*/
SQLConnector.prototype.buildSelect = function(model, filter, options) {
options = options || {};
filter = this._prepareFilter(model, filter);

var haveRelationFilters = this.hasRelationClause(model, filter.where);

var distinct = haveRelationFilters ? 'DISTINCT ' : '';

var selectStmt = new ParameterizedSQL('SELECT ' + distinct +
Expand Down Expand Up @@ -1077,34 +1193,50 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) {
* @param {object} where An object for the where conditions
* @returns {ParameterizedSQL} The SQL INNER JOIN clauses
*/
SQLConnector.prototype.buildJoins = function(model, where) {
SQLConnector.prototype.buildJoins = function(model, where, prevRelName) {
var modelDef = this.getModelDefinition(model);
var relations = modelDef.model.relations;
var stmt = new ParameterizedSQL('', []);

var self = this;
var buildOneToMany = function buildOneToMany(modelFrom, keyFrom, modelTo, keyTo, filter) {
var buildOneToMany = function buildOneToMany(relName, modelFrom, keyFrom, modelTo, keyTo, joinWhere,
forcePrevRelName) {
var ds1 = self.getDataSource(modelFrom);
var ds2 = self.getDataSource(modelTo);
assert(ds1 === ds2, 'Model ' + modelFrom + ' and ' + modelTo +
' must be attached to the same datasource');

var modelToEscaped = self.tableEscaped(modelTo);
var innerFilter = assign({}, filter);
var innerIdField = {};
innerIdField[keyTo] = true;
innerFilter.fields = assign({}, innerFilter.fields, innerIdField);
var relModel = self.getModelDefinition(modelTo);
var relRelations = relModel.model.relations;
var joinType = 'LEFT';

var condition = new ParameterizedSQL(
'ON ' +
self.columnEscaped(modelFrom, keyFrom, forcePrevRelName || prevRelName) + '=' +
self.columnEscaped(modelTo, keyTo, relName)
);

if (joinWhere) {
for (var key in joinWhere) {
if (key in relRelations) continue;

joinType = 'INNER';
break;
}

var condition = self.qualifiedColumnEscaped(modelFrom, keyFrom) + '=' +
self.qualifiedColumnEscaped(modelTo, keyTo);
var innerWhere = self._buildWhere(modelTo, joinWhere, relName);
if (innerWhere.sql.length) {
condition.merge(['AND', innerWhere]);
}
}

var innerSelect = self.buildSelect(modelTo, innerFilter, {
skipParameterize: true
});
var join = joinType + ' JOIN ' + modelToEscaped;
join += ' AS ' + self.escapeName(relName);

return new ParameterizedSQL('INNER JOIN (', [])
.merge(innerSelect)
.merge(') AS ' + modelToEscaped)
.merge('ON ' + condition);
return new ParameterizedSQL(join, [])
.merge(condition)
.merge(self.buildJoins(modelTo, joinWhere, relName));
};

for (var key in where) {
Expand All @@ -1114,21 +1246,24 @@ SQLConnector.prototype.buildJoins = function(model, where) {
var keyFrom = rel.keyFrom;
var modelTo = rel.modelTo.definition.name;
var keyTo = rel.keyTo;
var relName = rel.name;
if (prevRelName) {
relName = prevRelName + '_' + relName;
}

var join;
if (!rel.modelThrough) {
// 1:n relation
join = buildOneToMany(model, keyFrom, modelTo, keyTo, where[key]);
join = buildOneToMany(relName, model, keyFrom, modelTo, keyTo, where[key]);
} else {
// n:m relation
var modelThrough = rel.modelThrough.definition.name;
var keyThrough = rel.keyThrough;
var modelToKey = rel.modelTo.definition.idName();
var innerFilter = {fields: {}};
innerFilter.fields[keyThrough] = true;

var joinInner = buildOneToMany(model, keyFrom, modelThrough, keyTo, innerFilter);
join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key]);
var joinInner = buildOneToMany(relName + '_though', model, keyFrom, modelThrough, keyTo);
join = buildOneToMany(relName, modelThrough, keyThrough, modelTo, modelToKey, where[key],
relName + '_though');
join = joinInner.merge(join);
}
stmt.merge(join);
Expand Down Expand Up @@ -1268,6 +1403,7 @@ SQLConnector.prototype.hasRelationClause = function(model, where) {
* @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]}
*/
SQLConnector.prototype.buildCount = function(model, where, options) {
where = this._prepareFilter(model, { where: where }).where;
var haveRelationFilters = this.hasRelationClause(model, where);

var count = 'count(*)';
Expand Down
18 changes: 0 additions & 18 deletions lib/utils.js

This file was deleted.

Loading