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

add support native SQL join #403

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
176 changes: 166 additions & 10 deletions lib/sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -1112,11 +1112,16 @@ SQLConnector.prototype._buildWhere = function(model, where) {
return new ParameterizedSQL('');
}
const self = this;
const props = self.getModelDefinition(model).properties;

const modelDef = self.getModelDefinition(model);
const props = modelDef.properties;
const relations = modelDef.model.relations;
const whereStmts = [];
for (const key in where) {
const stmt = new ParameterizedSQL('', []);
if (relations && key in relations) {
// relationships are handled on joins
continue;
}
// Handle and/or operators
if (key === 'and' || key === 'or') {
const branches = [];
Expand Down Expand Up @@ -1239,10 +1244,24 @@ SQLConnector.prototype.buildOrderBy = function(model, order) {
const clauses = [];
for (let i = 0, n = order.length; i < n; i++) {
const t = order[i].split(/[\s,]+/);
let colName;
if (t[0].indexOf('.') < 0) {
colName = self.columnEscaped(model, t[0]);
} else {
// Column name is in the format: relationName.columnName
const colSplit = t[0].split('.');
// Find the name of the relation's model ...
const modelDef = this.getModelDefinition(model);
const relation = modelDef.model.relations[colSplit[0]];
const 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 @@ -1432,17 +1451,24 @@ SQLConnector.prototype.buildColumnNames = function(model, filter) {
* @returns {ParameterizedSQL} Statement object {sql: ..., params: ...}
*/
SQLConnector.prototype.buildSelect = function(model, filter, options) {
options = options || {};
if (!filter.order) {
const idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
}

let selectStmt = new ParameterizedSQL('SELECT ' +
const haveRelationFilters = this.hasRelationClause(model, filter.where);
const distinct = haveRelationFilters ? 'DISTINCT ' : '';

let selectStmt = new ParameterizedSQL('SELECT ' + distinct +
this.buildColumnNames(model, filter) +
' FROM ' + this.tableEscaped(model));

if (haveRelationFilters) {
const joinsStmts = this.buildJoins(model, filter.where);
selectStmt.merge(joinsStmts);
}
if (filter) {
if (filter.where) {
const whereStmt = this.buildWhere(model, filter.where);
Expand All @@ -1459,6 +1485,9 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) {
);
}
}
if (options.skipParameterize === true) {
return selectStmt;
}
return this.parameterize(selectStmt);
};

Expand Down Expand Up @@ -1582,10 +1611,7 @@ SQLConnector.prototype.count = function(model, where, options, cb) {
where = tmp;
}

let stmt = new ParameterizedSQL('SELECT count(*) as "cnt" FROM ' +
this.tableEscaped(model));
stmt = stmt.merge(this.buildWhere(model, where));
stmt = this.parameterize(stmt);
const stmt = this.buildCount(model, where, options);
this.execute(stmt.sql, stmt.params, options,
function(err, res) {
if (err) {
Expand Down Expand Up @@ -2143,3 +2169,133 @@ SQLConnector.prototype.setNullableProperty = function(property) {
throw new Error(g.f('{{setNullableProperty}} must be implemented by' +
'the connector'));
};
/**
* 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) {
let table = this.tableEscaped(model);
const index = table.indexOf('.');
if (index !== -1) {
// Remove the schema name
table = table.substring(index);
}
return table + '.' + this.escapeName(this.column(model, property));
};
/**
* 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) {
const modelDef = this.getModelDefinition(model);
const relations = modelDef.model.relations;
const stmt = new ParameterizedSQL('', []);

const self = this;
const buildOneToMany = function buildOneToMany(
modelFrom, keyFrom, modelTo, keyTo, filter,
) {
const ds1 = self.getDataSource(modelFrom);
const ds2 = self.getDataSource(modelTo);
assert(ds1 === ds2, 'Model ' + modelFrom + ' and ' + modelTo +
' must be attached to the same datasource');
const modelToEscaped = self.tableEscaped(modelTo);
const innerFilter = Object.assign({}, filter);
const innerIdField = {};
innerIdField[keyTo] = true;
innerFilter.fields = Object.assign({}, innerFilter.fields, innerIdField);

const condition = self.qualifiedColumnEscaped(modelFrom, keyFrom) + '=' +
self.qualifiedColumnEscaped(modelTo, keyTo);

const innerSelect = self.buildSelect(modelTo, innerFilter, {
skipParameterize: true,
});

return new ParameterizedSQL('INNER JOIN (', [])
.merge(innerSelect)
.merge(') AS ' + modelToEscaped)
.merge('ON ' + condition);
};

for (const key in where) {
if (!(key in relations)) continue;

const rel = relations[key];
const keyFrom = rel.keyFrom;
const modelTo = rel.modelTo.definition.name;
const keyTo = rel.keyTo;

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

const joinInner = buildOneToMany(model, keyFrom, modelThrough, keyTo, innerFilter);
join = buildOneToMany(modelThrough, keyThrough, modelTo, modelToKey, where[key]);
join = joinInner.merge(join);
}
stmt.merge(join);
}

return stmt;
};
/**
* Check if the where statement contains relations
* @param model
* @param where
* @returns {boolean}
*/
SQLConnector.prototype.hasRelationClause = function(model, where) {
let found = false;
if (where) {
const relations = this.getModelDefinition(model).model.relations;
if (relations) {
for (const key in where) {
if (key in relations) {
found = true;
break;
}
}
}
}
return found;
};
/**
* Build a SQL SELECT statement to count rows
* @param {String} model Model name
* @param {Object} where Where object
* @param {Object} options Options object
* @returns {ParameterizedSQL} Statement object {sql: ..., params: [...]}
*/
SQLConnector.prototype.buildCount = function(model, where, options) {
const haveRelationFilters = this.hasRelationClause(model, where);

let count = 'count(*)';
if (haveRelationFilters) {
const idColumn = this.columnEscaped(model, this.idColumn(model));
count = 'count(DISTINCT ' + idColumn + ')';
}

let stmt = new ParameterizedSQL('SELECT ' + count +
' as "cnt" FROM ' + this.tableEscaped(model));

if (haveRelationFilters) {
const joinsStmts = this.buildJoins(model, where);
stmt = stmt.merge(joinsStmts);
}

stmt = stmt.merge(this.buildWhere(model, where));
return this.parameterize(stmt);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"lint:fix": "eslint . --fix",
"posttest": "npm run lint",
"test": "npm run test:ci",
"test:ci": "nyc --reporter=lcov mocha"
"test:ci": "nyc --reporter=lcov mocha --debug-brk"
},
"license": "MIT",
"dependencies": {
Expand Down
Loading