不要怂,就是干,撸起袖子干!

Commit caa03cd5 by cbauerme Committed by Mick Hansen

Fixed behavior where a mixed required status in an include would not honor given required's (#6170)

* Fixed behavior where a mixed required status in an include would not be respected
 Refactored include handling of abstract query-generator into seperate functions for through or non-thorugh joins
 Join generating functions now return join in parts so that it can be rearranged by the parent in even of a `required` mismatch
 Internal table names that are a combonation for multiple tables are now seperated by underscores instead of periods to avoid bad behavior inside parentheses
 Multiple fixes to tests to accommodate changing behavior

* Fixes to PR
 Fixed spelling of "separate"
 generateInclude now returns new attributes instead of adding them to arrays passed in
 consolidated topLevel options into one object

* Removed line ketp in merge. Whoops!

* Separate fix, shoulkd fix failing psql tests.

* Removed errant logging from tests, added more nested include testing.

* Replaced '_' sperator with '->' in 'internal' selections.

* fixed wrong name in abstract query-generator.

* translate literal keys and  in select where to new internal -> connectors.

* Replaced needlessly verbose ternary operator in abstract query-generator.

* Fixed foramtting and nameing, added fix to changelog.

* Fixed variable shadowing issue from rename.

* Fixed sql unit test from master.

* Fixed Postgres unit test.

* added .eslintrs.json back to git.

* Fixed postgres identifier quoting issues.

* Added line about bc breaks to changlog.

* Removed lines from development, tightened spacing to match style.
1 parent b94a271b
...@@ -78,6 +78,7 @@ ...@@ -78,6 +78,7 @@
- [FIXED] All associations now prefer aliases to construct foreign key [#5267](https://github.com/sequelize/sequelize/issues/5267) - [FIXED] All associations now prefer aliases to construct foreign key [#5267](https://github.com/sequelize/sequelize/issues/5267)
- [REMOVED] Default transaction auto commit [#5094](https://github.com/sequelize/sequelize/issues/5094) - [REMOVED] Default transaction auto commit [#5094](https://github.com/sequelize/sequelize/issues/5094)
- [REMOVED] Callback support for hooks [#5228](https://github.com/sequelize/sequelize/issues/5228) - [REMOVED] Callback support for hooks [#5228](https://github.com/sequelize/sequelize/issues/5228)
- [FIXED] Setting required in a nested include will not force the parent include to be required as well [#5999](https://github.com/sequelize/sequelize/issues/5999)
## BC breaks: ## BC breaks:
- `hookValidate` removed in favor of `validate` with `hooks: true | false`. `validate` returns a promise which is rejected if validation fails - `hookValidate` removed in favor of `validate` with `hooks: true | false`. `validate` returns a promise which is rejected if validation fails
...@@ -95,6 +96,7 @@ ...@@ -95,6 +96,7 @@
- All associations type will prefer `as` when constructing the `foreignKey` name. You can override this by `foreignKey` option. - All associations type will prefer `as` when constructing the `foreignKey` name. You can override this by `foreignKey` option.
- Removed default `AUTO COMMIT` for transaction. Its only sent if explicitly set by user or required by dialects (like `mysql`) - Removed default `AUTO COMMIT` for transaction. Its only sent if explicitly set by user or required by dialects (like `mysql`)
- Hooks no longer provide a callback - you can return a `then`-able instead if you are doing async stuff - Hooks no longer provide a callback - you can return a `then`-able instead if you are doing async stuff
- Table names of a select query have change internally from 'originModel.associatedModel.field' to 'originModel->associatedModel.field'
# 3.23.2 # 3.23.2
- [FIXED] Type validation now works with non-strings due to updated validator@5.0.0 [#5861](https://github.com/sequelize/sequelize/pull/5861) - [FIXED] Type validation now works with non-strings due to updated validator@5.0.0 [#5861](https://github.com/sequelize/sequelize/pull/5861)
......
...@@ -613,7 +613,8 @@ const QueryGenerator = { ...@@ -613,7 +613,8 @@ const QueryGenerator = {
potentially also be used for other places where we want to be able to call SQL functions (e.g. as default values) potentially also be used for other places where we want to be able to call SQL functions (e.g. as default values)
@private @private
*/ */
quote(obj, parent, force) { quote(obj, parent, force, connector) {
connector = connector || '.';
if (Utils._.isString(obj)) { if (Utils._.isString(obj)) {
return this.quoteIdentifiers(obj, force); return this.quoteIdentifiers(obj, force);
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
...@@ -656,12 +657,12 @@ const QueryGenerator = { ...@@ -656,12 +657,12 @@ const QueryGenerator = {
parentAssociation = association; parentAssociation = association;
} else { } else {
tableNames[i] = model.tableName; tableNames[i] = model.tableName;
throw new Error('\'' + tableNames.join('.') + '\' in order / group clause is not valid association'); throw new Error('\'' + tableNames.join(connector) + '\' in order / group clause is not valid association');
} }
} }
// add 1st string as quoted, 2nd as unquoted raw // add 1st string as quoted, 2nd as unquoted raw
let sql = (i > 0 ? this.quoteIdentifier(tableNames.join('.')) + '.' : (Utils._.isString(obj[0]) && parent ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force); let sql = (i > 0 ? this.quoteIdentifier(tableNames.join(connector)) + '.' : (Utils._.isString(obj[0]) && parent ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force);
if (i < len - 1) { if (i < len - 1) {
if (obj[i + 1]._isSequelizeMethod) { if (obj[i + 1]._isSequelizeMethod) {
sql += this.handleSequelizeMethod(obj[i + 1]); sql += this.handleSequelizeMethod(obj[i + 1]);
...@@ -748,409 +749,93 @@ const QueryGenerator = { ...@@ -748,409 +749,93 @@ const QueryGenerator = {
- offset -> An offset value to start from. Only useable with limit! - offset -> An offset value to start from. Only useable with limit!
@private @private
*/ */
selectQuery(tableName, options, model) { selectQuery(tableName, options, model) {
// Enter and change at your own peril -- Mick Hansen
options = options || {}; options = options || {};
const limit = options.limit; const limit = options.limit;
const mainModel = model;
const mainQueryItems = []; const mainQueryItems = [];
const subQuery = options.subQuery === undefined ? limit && options.hasMultiAssociation : options.subQuery;
const subQueryItems = []; const subQueryItems = [];
let table = null; const subQuery = options.subQuery === undefined ? limit && options.hasMultiAssociation : options.subQuery;
let query; const attributes = {
let mainAttributes = options.attributes && options.attributes.slice(); main: options.attributes && options.attributes.slice(),
subQuery: null,
};
const mainTable = {
name: tableName,
quotedName: null,
as: null,
model: model,
};
const topLevelInfo = {
names: mainTable,
options,
subQuery,
};
let mainJoinQueries = []; let mainJoinQueries = [];
// We'll use a subquery if we have a hasMany association and a limit
let subQueryAttributes = null;
let subJoinQueries = []; let subJoinQueries = [];
let mainTableAs = null; let query;
// resolve table name options
if (options.tableAs) { if (options.tableAs) {
mainTableAs = this.quoteTable(options.tableAs); mainTable.as = this.quoteTable(options.tableAs);
} else if (!Array.isArray(tableName) && model) { } else if (!Array.isArray(mainTable.name) && mainTable.model) {
mainTableAs = this.quoteTable(model.name); mainTable.as = this.quoteTable(mainTable.model.name);
} }
table = !Array.isArray(tableName) ? this.quoteTable(tableName) : tableName.map(t => { mainTable.quotedName = !Array.isArray(mainTable.name) ? this.quoteTable(mainTable.name) : tableName.map(t => {
if (Array.isArray(t)) { return Array.isArray(t) ? this.quoteTable(t[0], t[1]) : this.quoteTable(t, true);
return this.quoteTable(t[0], t[1]);
}
return this.quoteTable(t, true);
}).join(', '); }).join(', ');
if (subQuery && mainAttributes) { if (subQuery && attributes.main) {
for (const keyAtt of model.primaryKeyAttributes) { for (const keyAtt of mainTable.model.primaryKeyAttributes) {
// Check if mainAttributes contain the primary key of the model either as a field or an aliased field // Check if mainAttributes contain the primary key of the model either as a field or an aliased field
if (!_.find(mainAttributes, attr => keyAtt === attr || keyAtt === attr[0] || keyAtt === attr[1])) { if (!_.find(attributes.main, attr => keyAtt === attr || keyAtt === attr[0] || keyAtt === attr[1])) {
mainAttributes.push(model.rawAttributes[keyAtt].field ? [keyAtt, model.rawAttributes[keyAtt].field] : keyAtt); attributes.main.push(mainTable.model.rawAttributes[keyAtt].field ? [keyAtt, mainTable.model.rawAttributes[keyAtt].field] : keyAtt);
} }
} }
} }
// Escape attributes attributes.main = this.escapeAttributes(attributes.main, options, mainTable.as);
mainAttributes = mainAttributes && mainAttributes.map(attr => { attributes.main = attributes.main || (options.include ? [`${mainTable.as}.*`] : ['*']);
let addTable = true;
if (attr._isSequelizeMethod) {
return this.handleSequelizeMethod(attr);
}
if (Array.isArray(attr)) {
if (attr.length !== 2) {
throw new Error(JSON.stringify(attr) + ' is not a valid attribute definition. Please use the following format: [\'attribute definition\', \'alias\']');
}
attr = attr.slice();
if (attr[0]._isSequelizeMethod) {
attr[0] = this.handleSequelizeMethod(attr[0]);
addTable = false;
} else if (attr[0].indexOf('(') === -1 && attr[0].indexOf(')') === -1) {
attr[0] = this.quoteIdentifier(attr[0]);
}
attr = [attr[0], this.quoteIdentifier(attr[1])].join(' AS ');
} else {
attr = attr.indexOf(Utils.TICK_CHAR) < 0 && attr.indexOf('"') < 0 ? this.quoteIdentifiers(attr) : attr;
}
if (options.include && attr.indexOf('.') === -1 && addTable) {
attr = mainTableAs + '.' + attr;
}
return attr;
});
// If no attributes specified, use *
mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery // If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery
if (subQuery || options.groupedLimit) { if (subQuery || options.groupedLimit) {
// We need primary keys // We need primary keys
subQueryAttributes = mainAttributes; attributes.subQuery = attributes.main;
mainAttributes = [(mainTableAs || table) + '.*']; attributes.main = [(mainTable.as || mainTable.quotedName) + '.*'];
} }
if (options.include) { if (options.include) {
const generateJoinQueries = (include, parentTable) => {
const association = include.association;
const through = include.through;
const joinType = include.required ? ' INNER JOIN ' : ' LEFT OUTER JOIN ';
const parentIsTop = !include.parent.association && include.parent.model.name === options.model.name;
const whereOptions = Utils._.clone(options);
const table = include.model.getTableName();
const joinQueries = {
mainQuery: [],
subQuery: []
};
let as = include.as;
let joinQueryItem = '';
let attributes;
let targetWhere;
whereOptions.keysEscaped = true;
if (tableName !== parentTable && mainTableAs !== parentTable) {
as = parentTable + '.' + include.as;
}
// includeIgnoreAttributes is used by aggregate functions
if (options.includeIgnoreAttributes !== false) {
attributes = include.attributes.map(attr => {
let attrAs = attr;
let verbatim = false;
if (Array.isArray(attr) && attr.length === 2) {
if (attr[0]._isSequelizeMethod) {
if (attr[0] instanceof Utils.Literal ||
attr[0] instanceof Utils.Cast ||
attr[0] instanceof Utils.Fn
) {
verbatim = true;
}
}
attr = attr.map(attr => attr._isSequelizeMethod ? this.handleSequelizeMethod(attr) : attr);
attrAs = attr[1];
attr = attr[0];
} else if (attr instanceof Utils.Literal) {
return attr.val; // We trust the user to rename the field correctly
} else if (attr instanceof Utils.Cast || attr instanceof Utils.Fn) {
throw new Error(
'Tried to select attributes using Sequelize.cast or Sequelize.fn without specifying an alias for the result, during eager loading. ' +
'This means the attribute will not be added to the returned instance'
);
}
let prefix;
if (verbatim === true) {
prefix = attr;
} else {
prefix = this.quoteIdentifier(as) + '.' + this.quoteIdentifier(attr);
}
return prefix + ' AS ' + this.quoteIdentifier(as + '.' + attrAs, true);
});
if (include.subQuery && subQuery) {
subQueryAttributes = subQueryAttributes.concat(attributes);
} else {
mainAttributes = mainAttributes.concat(attributes);
}
}
if (through) {
const throughTable = through.model.getTableName();
const throughAs = as + '.' + through.as;
const throughAttributes = through.attributes.map(attr =>
this.quoteIdentifier(throughAs) + '.' + this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)
+ ' AS '
+ this.quoteIdentifier(throughAs + '.' + (Array.isArray(attr) ? attr[1] : attr))
);
const primaryKeysSource = association.source.primaryKeyAttributes;
const tableSource = parentTable;
const identSource = association.identifierField;
const primaryKeysTarget = association.target.primaryKeyAttributes;
const tableTarget = as;
const identTarget = association.foreignIdentifierField;
const attrTarget = association.target.rawAttributes[primaryKeysTarget[0]].field || primaryKeysTarget[0];
let attrSource = primaryKeysSource[0];
let sourceJoinOn;
let targetJoinOn;
let throughWhere;
if (options.includeIgnoreAttributes !== false) {
// Through includes are always hasMany, so we need to add the attributes to the mainAttributes no matter what (Real join will never be executed in subquery)
mainAttributes = mainAttributes.concat(throughAttributes);
}
// Figure out if we need to use field or attribute
if (!subQuery) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
}
if (subQuery && !include.subQuery && !include.parent.subQuery && include.parent.model !== mainModel) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
}
// Filter statement for left side of through
// Used by both join and subquery where
// If parent include was in a subquery need to join on the aliased attribute
if (subQuery && !include.subQuery && include.parent.subQuery && !parentIsTop) {
sourceJoinOn = this.quoteIdentifier(tableSource + '.' + attrSource) + ' = ';
} else {
sourceJoinOn = this.quoteTable(tableSource) + '.' + this.quoteIdentifier(attrSource) + ' = ';
}
sourceJoinOn += this.quoteIdentifier(throughAs) + '.' + this.quoteIdentifier(identSource);
// Filter statement for right side of through
// Used by both join and subquery where
targetJoinOn = this.quoteIdentifier(tableTarget) + '.' + this.quoteIdentifier(attrTarget) + ' = ';
targetJoinOn += this.quoteIdentifier(throughAs) + '.' + this.quoteIdentifier(identTarget);
if (include.through.where) {
throughWhere = this.getWhereConditions(include.through.where, this.sequelize.literal(this.quoteIdentifier(throughAs)), include.through.model);
}
if (this._dialect.supports.joinTableDependent) {
// Generate a wrapped join so that the through table join can be dependent on the target join
joinQueryItem += joinType + '(';
joinQueryItem += this.quoteTable(throughTable, throughAs);
joinQueryItem += ' INNER JOIN ' + this.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
if (throughWhere) {
joinQueryItem += ' AND ' + throughWhere;
}
joinQueryItem += ') ON '+sourceJoinOn;
} else {
// Generate join SQL for left side of through
joinQueryItem += joinType + this.quoteTable(throughTable, throughAs) + ' ON ';
joinQueryItem += sourceJoinOn;
// Generate join SQL for right side of through
joinQueryItem += joinType + this.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
if (throughWhere) {
joinQueryItem += ' AND ' + throughWhere;
}
}
if (include.where || include.through.where) {
if (include.where) {
targetWhere = this.getWhereConditions(include.where, this.sequelize.literal(this.quoteIdentifier(as)), include.model, whereOptions);
if (targetWhere) {
joinQueryItem += ' AND ' + targetWhere;
}
}
if (subQuery && include.required) {
if (!options.where) options.where = {};
let parent = include;
let child = include;
let nestedIncludes = [];
let query;
while (parent = parent.parent) {
nestedIncludes = [_.extend({}, child, {include: nestedIncludes})];
child = parent;
}
const topInclude = nestedIncludes[0];
const topParent = topInclude.parent;
if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
query = this.selectQuery(topInclude.through.model.getTableName(), {
attributes: [topInclude.through.model.primaryKeyField],
include: Model._validateIncludedElements({
model: topInclude.through.model,
include: [{
association: topInclude.association.toTarget,
required: true
}]
}).include,
model: topInclude.through.model,
where: { $and: [
this.sequelize.asIs([
this.quoteTable(topParent.model.name) + '.' + this.quoteIdentifier(topParent.model.primaryKeyField),
this.quoteIdentifier(topInclude.through.model.name) + '.' + this.quoteIdentifier(topInclude.association.identifierField)
].join(' = ')),
topInclude.through.where
]},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.through.model);
} else {
query = this.selectQuery(topInclude.model.tableName, {
attributes: [topInclude.model.primaryKeyAttributes[0]],
include: topInclude.include,
where: {
$join: this.sequelize.asIs([
this.quoteTable(topParent.model.name) + '.' + this.quoteIdentifier(topParent.model.primaryKeyAttributes[0]),
this.quoteIdentifier(topInclude.model.name) + '.' + this.quoteIdentifier(topInclude.association.identifierField)
].join(' = '))
},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.model);
}
options.where['__' + throughAs] = this.sequelize.asIs([
'(',
query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
}
}
} else {
if (subQuery && include.subQueryFilter) {
const associationWhere = {};
associationWhere[association.identifierField] = {
$raw: this.quoteTable(parentTable) + '.' + this.quoteIdentifier(association.source.primaryKeyField)
};
if (!options.where) options.where = {};
// Creating the as-is where for the subQuery, checks that the required association exists
const $query = this.selectQuery(include.model.getTableName(), {
attributes: [association.identifierField],
where: {
$and: [
associationWhere,
include.where || {}
]
},
limit: 1
}, include.model);
const subQueryWhere = this.sequelize.asIs([
'(',
$query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
if (Utils._.isPlainObject(options.where)) {
options.where['__' + as] = subQueryWhere;
} else {
options.where = { $and: [options.where, subQueryWhere] };
}
}
joinQueryItem = ' ' + this.joinIncludeQuery({
model: mainModel,
subQuery: options.subQuery,
include,
groupedLimit: options.groupedLimit
});
}
if (include.subQuery && subQuery) {
joinQueries.subQuery.push(joinQueryItem);
} else {
joinQueries.mainQuery.push(joinQueryItem);
}
if (include.include) {
for (const childInclude of include.include) {
if (childInclude.separate || childInclude._pseudo) {
continue;
}
const childJoinQueries = generateJoinQueries(childInclude, as);
if (childInclude.subQuery && subQuery) {
joinQueries.subQuery = joinQueries.subQuery.concat(childJoinQueries.subQuery);
}
if (childJoinQueries.mainQuery) {
joinQueries.mainQuery = joinQueries.mainQuery.concat(childJoinQueries.mainQuery);
}
}
}
return joinQueries;
};
// Loop through includes and generate subqueries
for (const include of options.include) { for (const include of options.include) {
if (include.separate) { if (include.separate) {
continue; continue;
} }
const joinQueries = this.generateInclude(include, { externalAs: mainTable.as, internalAs: mainTable.as }, topLevelInfo);
const joinQueries = generateJoinQueries(include, mainTableAs);
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery); subJoinQueries = subJoinQueries.concat(joinQueries.subQuery);
mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery); mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery);
if (joinQueries.attributes.main.length > 0) {
attributes.main = attributes.main.concat(joinQueries.attributes.main);
}
if (joinQueries.attributes.subQuery.length > 0) {
attributes.subQuery = attributes.subQuery.concat(joinQueries.attributes.subQuery);
}
} }
} }
// If using subQuery select defined subQuery attributes and join subJoinQueries
if (subQuery) { if (subQuery) {
subQueryItems.push(this.selectFromTableFragment(options, model, subQueryAttributes, table, mainTableAs)); subQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.subQuery, mainTable.quotedName, mainTable.as));
subQueryItems.push(subJoinQueries.join('')); subQueryItems.push(subJoinQueries.join(''));
// Else do it the reguar way
} else { } else {
if (options.groupedLimit) { if (options.groupedLimit) {
if (!mainTableAs) { if (!mainTable.as) {
mainTableAs = table; mainTable.as = mainTable.quotedName;
} }
const where = Object.assign({}, options.where); const where = Object.assign({}, options.where);
let groupedLimitOrder let groupedLimitOrder
, whereKey , whereKey
, include , include
, groupedTableName = mainTableAs; , groupedTableName = mainTable.as;
if (typeof options.groupedLimit.on === 'string') { if (typeof options.groupedLimit.on === 'string') {
whereKey = options.groupedLimit.on; whereKey = options.groupedLimit.on;
...@@ -1210,7 +895,7 @@ const QueryGenerator = { ...@@ -1210,7 +895,7 @@ const QueryGenerator = {
// Caching the base query and splicing the where part into it is consistently > twice // Caching the base query and splicing the where part into it is consistently > twice
// as fast than generating from scratch each time for values.length >= 5 // as fast than generating from scratch each time for values.length >= 5
const baseQuery = '('+this.selectQuery( const baseQuery = '(' + this.selectQuery(
tableName, tableName,
{ {
attributes: options.attributes, attributes: options.attributes,
...@@ -1221,11 +906,11 @@ const QueryGenerator = { ...@@ -1221,11 +906,11 @@ const QueryGenerator = {
model model
}, },
model model
).replace(/;$/, '')+')'; ).replace(/;$/, '') + ')';
const placeHolder = this.whereItemQuery('$$PLACEHOLDER$$', true, { model }); const placeHolder = this.whereItemQuery('$$PLACEHOLDER$$', true, { model });
const splicePos = baseQuery.indexOf(placeHolder); const splicePos = baseQuery.indexOf(placeHolder);
mainQueryItems.push(this.selectFromTableFragment(options, model, mainAttributes, '('+ mainQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.main, '(' +
options.groupedLimit.values.map(value => { options.groupedLimit.values.map(value => {
let groupWhere; let groupWhere;
if (whereKey) { if (whereKey) {
...@@ -1241,18 +926,19 @@ const QueryGenerator = { ...@@ -1241,18 +926,19 @@ const QueryGenerator = {
return Utils.spliceStr(baseQuery, splicePos, placeHolder.length, this.getWhereConditions(groupWhere, groupedTableName)); return Utils.spliceStr(baseQuery, splicePos, placeHolder.length, this.getWhereConditions(groupWhere, groupedTableName));
}).join( }).join(
this._dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ' this._dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION '
) )
+')', mainTableAs)); + ')', mainTable.as));
} else { } else {
mainQueryItems.push(this.selectFromTableFragment(options, model, mainAttributes, table, mainTableAs)); mainQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.main, mainTable.quotedName, mainTable.as));
} }
mainQueryItems.push(mainJoinQueries.join('')); mainQueryItems.push(mainJoinQueries.join(''));
} }
// Add WHERE to sub or main query // Add WHERE to sub or main query
if (options.hasOwnProperty('where') && !options.groupedLimit) { if (options.hasOwnProperty('where') && !options.groupedLimit) {
options.where = this.getWhereConditions(options.where, mainTableAs || tableName, model, options); options.where = this.getWhereConditions(options.where, mainTable.as || tableName, model, options);
if (options.where) { if (options.where) {
if (subQuery) { if (subQuery) {
subQueryItems.push(' WHERE ' + options.where); subQueryItems.push(' WHERE ' + options.where);
...@@ -1260,8 +946,8 @@ const QueryGenerator = { ...@@ -1260,8 +946,8 @@ const QueryGenerator = {
mainQueryItems.push(' WHERE ' + options.where); mainQueryItems.push(' WHERE ' + options.where);
// Walk the main query to update all selects // Walk the main query to update all selects
_.each(mainQueryItems, (value, key) => { _.each(mainQueryItems, (value, key) => {
if(value.match(/^SELECT/)) { if (value.match(/^SELECT/)) {
mainQueryItems[key] = this.selectFromTableFragment(options, model, mainAttributes, table, mainTableAs, options.where); mainQueryItems[key] = this.selectFromTableFragment(options, model, attributes.main, mainTable.quotedName, mainTable.as, options.where);
} }
}); });
} }
...@@ -1287,10 +973,10 @@ const QueryGenerator = { ...@@ -1287,10 +973,10 @@ const QueryGenerator = {
mainQueryItems.push(' HAVING ' + options.having); mainQueryItems.push(' HAVING ' + options.having);
} }
} }
// Add ORDER to sub or main query // Add ORDER to sub or main query
if (options.order) { if (options.order) {
const orders = this.getQueryOrders(options, model, subQuery); const orders = this.getQueryOrders(options, model, subQuery);
if (orders.mainQueryOrder.length) { if (orders.mainQueryOrder.length) {
mainQueryItems.push(' ORDER BY ' + orders.mainQueryOrder.join(', ')); mainQueryItems.push(' ORDER BY ' + orders.mainQueryOrder.join(', '));
} }
...@@ -1300,7 +986,7 @@ const QueryGenerator = { ...@@ -1300,7 +986,7 @@ const QueryGenerator = {
} }
// Add LIMIT, OFFSET to sub or main query // Add LIMIT, OFFSET to sub or main query
const limitOrder = this.addLimitAndOffset(options, model); const limitOrder = this.addLimitAndOffset(options, mainTable.model);
if (limitOrder && !options.groupedLimit) { if (limitOrder && !options.groupedLimit) {
if (subQuery) { if (subQuery) {
subQueryItems.push(limitOrder); subQueryItems.push(limitOrder);
...@@ -1309,13 +995,8 @@ const QueryGenerator = { ...@@ -1309,13 +995,8 @@ const QueryGenerator = {
} }
} }
// If using subQuery, select attributes from wrapped subQuery and join out join tables
if (subQuery) { if (subQuery) {
query = 'SELECT ' + mainAttributes.join(', ') + ' FROM ('; query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) AS ${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`;
query += subQueryItems.join('');
query += ') AS ' + mainTableAs;
query += mainJoinQueries.join('');
query += mainQueryItems.join('');
} else { } else {
query = mainQueryItems.join(''); query = mainQueryItems.join('');
} }
...@@ -1337,128 +1018,268 @@ const QueryGenerator = { ...@@ -1337,128 +1018,268 @@ const QueryGenerator = {
} }
} }
query += ';'; return `${query};`;
return query;
}, },
getQueryOrders(options, model, subQuery) { escapeAttributes(attributes, options, mainTableAs) {
const mainQueryOrder = []; return attributes && attributes.map(attr => {
const subQueryOrder = []; let addTable = true;
const validateOrder = order => { if (attr._isSequelizeMethod) {
if (order instanceof Utils.Literal) return; return this.handleSequelizeMethod(attr);
}
if (Array.isArray(attr)) {
if (attr.length !== 2) {
throw new Error(JSON.stringify(attr) + ' is not a valid attribute definition. Please use the following format: [\'attribute definition\', \'alias\']');
}
attr = attr.slice();
if (!_.includes([ if (attr[0]._isSequelizeMethod) {
'ASC', attr[0] = this.handleSequelizeMethod(attr[0]);
'DESC', addTable = false;
'ASC NULLS LAST', } else if (attr[0].indexOf('(') === -1 && attr[0].indexOf(')') === -1) {
'DESC NULLS LAST', attr[0] = this.quoteIdentifier(attr[0]);
'ASC NULLS FIRST', }
'DESC NULLS FIRST', attr = [attr[0], this.quoteIdentifier(attr[1])].join(' AS ');
'NULLS FIRST', } else {
'NULLS LAST' attr = attr.indexOf(Utils.TICK_CHAR) < 0 && attr.indexOf('"') < 0 ? this.quoteIdentifiers(attr) : attr;
], order.toUpperCase())) { }
throw new Error(util.format('Order must be \'ASC\' or \'DESC\', \'%s\' given', order)); if (options.include && attr.indexOf('.') === -1 && addTable) {
attr = mainTableAs + '.' + attr;
} }
return attr;
});
},
generateInclude(include, parentTableName, topLevelInfo) {
const association = include.association;
const joinQueries = {
mainQuery: [],
subQuery: [],
};
const mainChildIncludes = [];
const subChildIncludes = [];
let requiredMismatch = false;
let includeAs = {
internalAs: include.as,
externalAs: include.as
}; };
let attributes = {
main: [],
subQuery: [],
};
let joinQuery;
if (Array.isArray(options.order)) { topLevelInfo.options.keysEscaped = true;
for (const t of options.order) {
if (Array.isArray(t) && _.size(t) > 1) { if (topLevelInfo.names.name !== parentTableName.externalAs && topLevelInfo.names.as !== parentTableName.externalAs) {
if ((typeof t[0] === 'function' && t[0].prototype instanceof Model) || (typeof t[0].model === 'function' && t[0].model.prototype instanceof Model)) { includeAs.internalAs = `${parentTableName.internalAs}->${include.as}`;
if (typeof t[t.length - 2] === 'string') { includeAs.externalAs = `${parentTableName.externalAs}.${include.as}`;
validateOrder(_.last(t)); }
// includeIgnoreAttributes is used by aggregate functions
if (topLevelInfo.options.includeIgnoreAttributes !== false) {
let includeAttributes = include.attributes.map(attr => {
let attrAs = attr;
let verbatim = false;
if (Array.isArray(attr) && attr.length === 2) {
if (attr[0]._isSequelizeMethod) {
if (attr[0] instanceof Utils.Literal ||
attr[0] instanceof Utils.Cast ||
attr[0] instanceof Utils.Fn
) {
verbatim = true;
} }
} else {
validateOrder(_.last(t));
} }
}
if (subQuery && (Array.isArray(t) && !(typeof t[0] === 'function' && t[0].prototype instanceof Model) && !(t[0] && typeof t[0].model === 'function' && t[0].model.prototype instanceof Model))) { attr = attr.map(attr => attr._isSequelizeMethod ? this.handleSequelizeMethod(attr) : attr);
subQueryOrder.push(this.quote(t, model));
attrAs = attr[1];
attr = attr[0];
} else if (attr instanceof Utils.Literal) {
return attr.val; // We trust the user to rename the field correctly
} else if (attr instanceof Utils.Cast || attr instanceof Utils.Fn) {
throw new Error(
'Tried to select attributes using Sequelize.cast or Sequelize.fn without specifying an alias for the result, during eager loading. ' +
'This means the attribute will not be added to the returned instance'
);
} }
mainQueryOrder.push(this.quote(t, model)); let prefix;
if (verbatim === true) {
prefix = attr;
} else {
prefix = `${this.quoteIdentifier(includeAs.internalAs)}.${this.quoteIdentifier(attr)}`;
}
return `${prefix} AS ${this.quoteIdentifier(`${includeAs.externalAs}.${attrAs}`, true)}`;
});
if (include.subQuery && topLevelInfo.subQuery) {
for (let attr of includeAttributes) {
attributes.subQuery.push(attr);
}
} else {
for (let attr of includeAttributes) {
attributes.main.push(attr);
}
} }
}
//through
if (include.through) {
joinQuery = this.generateThroughJoin(include, includeAs, parentTableName.internalAs, topLevelInfo);
} else { } else {
var sql = this.quote(typeof options.order === 'string' ? new Utils.Literal(options.order) : options.order, model); if (topLevelInfo.subQuery && include.subQueryFilter) {
if (subQuery) { const associationWhere = {};
subQueryOrder.push(sql);
associationWhere[association.identifierField] = {
$raw: `${this.quoteTable(parentTableName.internalAs)}.${this.quoteIdentifier(association.source.primaryKeyField)}`
};
if (!topLevelInfo.options.where) {
topLevelInfo.options.where = {};
}
// Creating the as-is where for the subQuery, checks that the required association exists
const $query = this.selectQuery(include.model.getTableName(), {
attributes: [association.identifierField],
where: {
$and: [
associationWhere,
include.where || {}
]
},
limit: 1
}, include.model);
const subQueryWhere = this.sequelize.asIs([
'(',
$query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
if (Utils._.isPlainObject(topLevelInfo.options.where)) {
topLevelInfo.options.where['__' + includeAs] = subQueryWhere;
} else {
topLevelInfo.options.where = { $and: [topLevelInfo.options.where, subQueryWhere] };
}
} }
mainQueryOrder.push(sql); joinQuery = this.generateJoin(include, topLevelInfo);
} }
return {mainQueryOrder, subQueryOrder}; // handle possible new attributes created in join
}, if (joinQuery.attributes.main.length > 0) {
attributes.main = attributes.main.concat(joinQuery.attributes.main);
}
selectFromTableFragment(options, model, attributes, tables, mainTableAs) { if (joinQuery.attributes.subQuery.length > 0) {
let fragment = 'SELECT ' + attributes.join(', ') + ' FROM ' + tables; attributes.subQuery = attributes.subQuery.concat(joinQuery.attributes.subQuery);
}
if(mainTableAs) { if (include.include) {
fragment += ' AS ' + mainTableAs; for (const childInclude of include.include) {
if (childInclude.separate || childInclude._pseudo) {
continue;
}
const childJoinQueries = this.generateInclude(childInclude, includeAs, topLevelInfo);
if (include.required === false && childInclude.required === true) {
requiredMismatch = true;
}
// if the child is a sub query we just give it to the
if (childInclude.subQuery && topLevelInfo.subQuery) {
subChildIncludes.push(childJoinQueries.subQuery);
}
if (childJoinQueries.mainQuery) {
mainChildIncludes.push(childJoinQueries.mainQuery);
}
if (childJoinQueries.attributes.main.length > 0) {
attributes.main = attributes.main.concat(childJoinQueries.attributes.main);
}
if (childJoinQueries.attributes.subQuery.length > 0) {
attributes.subQuery = attributes.subQuery.concat(childJoinQueries.attributes.subQuery);
}
}
} }
return fragment; if (include.subQuery && topLevelInfo.subQuery) {
if (requiredMismatch && subChildIncludes.length > 0) {
joinQueries.subQuery.push(` ${joinQuery.join} ( ${joinQuery.body}${subChildIncludes.join('')} ) ON ${joinQuery.condition}`);
} else {
joinQueries.subQuery.push(` ${joinQuery.join} ${joinQuery.body} ON ${joinQuery.condition}`);
if (subChildIncludes.length > 0) {
joinQueries.subQuery.push(subChildIncludes.join(''));
}
}
joinQueries.mainQuery.push(mainChildIncludes.join(''));
} else {
if (requiredMismatch && mainChildIncludes.length > 0) {
joinQueries.mainQuery.push(` ${joinQuery.join} ( ${joinQuery.body}${mainChildIncludes.join('')} ) ON ${joinQuery.condition}`);
} else {
joinQueries.mainQuery.push(` ${joinQuery.join} ${joinQuery.body} ON ${joinQuery.condition}`);
if (mainChildIncludes.length > 0) {
joinQueries.mainQuery.push(mainChildIncludes.join(''));
}
}
joinQueries.subQuery.push(subChildIncludes.join(''));
}
return {
mainQuery: joinQueries.mainQuery.join(''),
subQuery: joinQueries.subQuery.join(''),
attributes: attributes,
};
}, },
joinIncludeQuery(options) { generateJoin(include, topLevelInfo) {
const subQuery = options.subQuery;
const include = options.include;
const association = include.association; const association = include.association;
const parent = include.parent; const parent = include.parent;
const parentIsTop = !include.parent.association && include.parent.model.name === options.model.name; const parentIsTop = !!parent && !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
const joinType = include.required ? 'INNER JOIN ' : 'LEFT OUTER JOIN ';
let $parent; let $parent;
let joinWhere; let joinWhere;
/* Attributes for the left side */ /* Attributes for the left side */
const left = association.source; const left = association.source;
const attrLeft = association instanceof BelongsTo ? const attrLeft = association instanceof BelongsTo ?
association.identifier : association.identifier :
left.primaryKeyAttribute; left.primaryKeyAttribute;
const fieldLeft = association instanceof BelongsTo ? const fieldLeft = association instanceof BelongsTo ?
association.identifierField : association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field; left.rawAttributes[left.primaryKeyAttribute].field;
let asLeft; let asLeft;
/* Attributes for the right side */ /* Attributes for the right side */
const right = include.model; const right = include.model;
const tableRight = right.getTableName(); const tableRight = right.getTableName();
const fieldRight = association instanceof BelongsTo ? const fieldRight = association instanceof BelongsTo ?
right.rawAttributes[association.targetIdentifier || right.primaryKeyAttribute].field : right.rawAttributes[association.targetIdentifier || right.primaryKeyAttribute].field :
association.identifierField; association.identifierField;
let asRight = include.as; let asRight = include.as;
while (($parent = ($parent && $parent.parent || include.parent)) && $parent.association) { while (($parent = ($parent && $parent.parent || include.parent)) && $parent.association) {
if (asLeft) { if (asLeft) {
asLeft = [$parent.as, asLeft].join('.'); asLeft = `${$parent.as}->${asLeft}`;
} else { } else {
asLeft = $parent.as; asLeft = $parent.as;
} }
} }
if (!asLeft) asLeft = parent.as || parent.model.name; if (!asLeft) asLeft = parent.as || parent.model.name;
else asRight = [asLeft, asRight].join('.'); else asRight = `${asLeft}->${asRight}`;
let joinOn = [ let joinOn = `${this.quoteTable(asLeft)}.${this.quoteIdentifier(fieldLeft)}`;
this.quoteTable(asLeft),
this.quoteIdentifier(fieldLeft)
].join('.');
if ((options.groupedLimit && parentIsTop) || (subQuery && include.parent.subQuery && !include.subQuery)) { if ((topLevelInfo.options.groupedLimit && parentIsTop) || (topLevelInfo.subQuery && include.parent.subQuery && !include.subQuery)) {
if (parentIsTop) { if (parentIsTop) {
// The main model attributes is not aliased to a prefix // The main model attributes is not aliased to a prefix
joinOn = [ joinOn = `${this.quoteTable(parent.as || parent.model.name)}.${this.quoteIdentifier(attrLeft)}`;
this.quoteTable(parent.as || parent.model.name),
this.quoteIdentifier(attrLeft)
].join('.');
} else { } else {
joinOn = this.quoteIdentifier(asLeft + '.' + attrLeft); joinOn = this.quoteIdentifier(`${asLeft}.${attrLeft}`);
} }
} }
joinOn += ' = ' + this.quoteIdentifier(asRight) + '.' + this.quoteIdentifier(fieldRight); joinOn += ` = ${this.quoteIdentifier(asRight)}.${this.quoteIdentifier(fieldRight)}`;
if (include.on) { if (include.on) {
joinOn = this.whereItemsQuery(include.on, { joinOn = this.whereItemsQuery(include.on, {
...@@ -1474,14 +1295,244 @@ const QueryGenerator = { ...@@ -1474,14 +1295,244 @@ const QueryGenerator = {
}); });
if (joinWhere) { if (joinWhere) {
if (include.or) { if (include.or) {
joinOn += ' OR ' + joinWhere; joinOn += ` OR ${joinWhere}`;
} else {
joinOn += ` AND ${joinWhere}`;
}
}
}
return {
join: include.required ? 'INNER JOIN' : 'LEFT OUTER JOIN',
body: this.quoteTable(tableRight, asRight),
condition: joinOn,
attributes: {
main: [],
subQuery: [],
}
};
},
generateThroughJoin(include, includeAs, parentTableName, topLevelInfo) {
const through = include.through;
const throughTable = through.model.getTableName();
const throughAs = `${includeAs.internalAs}->${through.as}`;
const externalThroughAs = `${includeAs.externalAs}.${through.as}`;
const throughAttributes = through.attributes.map(attr =>
this.quoteIdentifier(throughAs) + '.' + this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)
+ ' AS '
+ this.quoteIdentifier(externalThroughAs + '.' + (Array.isArray(attr) ? attr[1] : attr))
);
const association = include.association;
const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
const primaryKeysSource = association.source.primaryKeyAttributes;
const tableSource = parentTableName;
const identSource = association.identifierField;
const primaryKeysTarget = association.target.primaryKeyAttributes;
const tableTarget = includeAs.internalAs;
const identTarget = association.foreignIdentifierField;
const attrTarget = association.target.rawAttributes[primaryKeysTarget[0]].field || primaryKeysTarget[0];
let joinType = include.required ? 'INNER JOIN' : 'LEFT OUTER JOIN';
let joinBody;
let joinCondition;
let attributes = {
main: [],
subQuery: [],
};
let attrSource = primaryKeysSource[0];
let sourceJoinOn;
let targetJoinOn;
let throughWhere;
let targetWhere;
if (topLevelInfo.options.includeIgnoreAttributes !== false) {
// Through includes are always hasMany, so we need to add the attributes to the mainAttributes no matter what (Real join will never be executed in subquery)
for (let attr of throughAttributes) {
attributes.main.push(attr);
}
}
// Figure out if we need to use field or attribute
if (!topLevelInfo.subQuery) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
}
if (topLevelInfo.subQuery && !include.subQuery && !include.parent.subQuery && include.parent.model !== topLevelInfo.options.mainModel) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
}
// Filter statement for left side of through
// Used by both join and subquery where
// If parent include was in a subquery need to join on the aliased attribute
if (topLevelInfo.subQuery && !include.subQuery && include.parent.subQuery && !parentIsTop) {
sourceJoinOn = `${this.quoteIdentifier(`${tableSource}.${attrSource}`)} = `;
} else {
sourceJoinOn = `${this.quoteTable(tableSource)}.${this.quoteIdentifier(attrSource)} = `;
}
sourceJoinOn += `${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(identSource)}`;
// Filter statement for right side of through
// Used by both join and subquery where
targetJoinOn = `${this.quoteIdentifier(tableTarget)}.${this.quoteIdentifier(attrTarget)} = `;
targetJoinOn += `${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(identTarget)}`;
if (through.where) {
throughWhere = this.getWhereConditions(through.where, this.sequelize.literal(this.quoteIdentifier(throughAs)), through.model);
}
if (this._dialect.supports.joinTableDependent) {
// Generate a wrapped join so that the through table join can be dependent on the target join
joinBody = `( ${this.quoteTable(throughTable, throughAs)} INNER JOIN ${this.quoteTable(include.model.getTableName(), includeAs.internalAs)} ON ${targetJoinOn}`;
if (throughWhere) {
joinBody += ` AND ${throughWhere}`;
}
joinBody += ')';
joinCondition = sourceJoinOn;
} else {
// Generate join SQL for left side of through
joinBody = `${this.quoteTable(throughTable, throughAs)} ON ${sourceJoinOn} ${joinType} ${this.quoteTable(include.model.getTableName(), includeAs.internalAs)}`;
joinCondition = targetJoinOn;
if (throughWhere) {
joinCondition += ` AND ${throughWhere}`;
}
}
if (include.where || include.through.where) {
if (include.where) {
targetWhere = this.getWhereConditions(include.where, this.sequelize.literal(this.quoteIdentifier(includeAs.internalAs)), include.model, topLevelInfo.options);
if (targetWhere) {
joinCondition += ` AND ${targetWhere}`;
}
}
if (topLevelInfo.subQuery && include.required) {
if (!topLevelInfo.options.where) {
topLevelInfo.options.where = {};
}
let parent = include;
let child = include;
let nestedIncludes = [];
let query;
while (parent = parent.parent) {
nestedIncludes = [_.extend({}, child, { include: nestedIncludes })];
child = parent;
}
const topInclude = nestedIncludes[0];
const topParent = topInclude.parent;
if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
query = this.selectQuery(topInclude.through.model.getTableName(), {
attributes: [topInclude.through.model.primaryKeyField],
include: Model._validateIncludedElements({
model: topInclude.through.model,
include: [{
association: topInclude.association.toTarget,
required: true
}]
}).include,
model: topInclude.through.model,
where: {
$and: [
this.sequelize.asIs([
this.quoteTable(topParent.model.name) + '.' + this.quoteIdentifier(topParent.model.primaryKeyField),
this.quoteIdentifier(topInclude.through.model.name) + '.' + this.quoteIdentifier(topInclude.association.identifierField)
].join(' = ')),
topInclude.through.where
]
},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.through.model);
} else { } else {
joinOn += ' AND ' + joinWhere; query = this.selectQuery(topInclude.model.tableName, {
attributes: [topInclude.model.primaryKeyAttributes[0]],
include: topInclude.include,
where: {
$join: this.sequelize.asIs([
this.quoteTable(topParent.model.name) + '.' + this.quoteIdentifier(topParent.model.primaryKeyAttributes[0]),
this.quoteIdentifier(topInclude.model.name) + '.' + this.quoteIdentifier(topInclude.association.identifierField)
].join(' = '))
},
limit: 1,
includeIgnoreAttributes: false
}, topInclude.model);
} }
topLevelInfo.options.where['__' + throughAs] = this.sequelize.asIs([
'(',
query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
} }
} }
return joinType + this.quoteTable(tableRight, asRight) + ' ON ' + joinOn; return {
join: joinType,
body: joinBody,
condition: joinCondition,
attributes: attributes,
};
},
getQueryOrders(options, model, subQuery) {
const mainQueryOrder = [];
const subQueryOrder = [];
const validateOrder = order => {
if (order instanceof Utils.Literal) return;
if (!_.includes([
'ASC',
'DESC',
'ASC NULLS LAST',
'DESC NULLS LAST',
'ASC NULLS FIRST',
'DESC NULLS FIRST',
'NULLS FIRST',
'NULLS LAST'
], order.toUpperCase())) {
throw new Error(util.format('Order must be \'ASC\' or \'DESC\', \'%s\' given', order));
}
};
if (Array.isArray(options.order)) {
for (const t of options.order) {
if (Array.isArray(t) && _.size(t) > 1) {
if ((typeof t[0] === 'function' && t[0].prototype instanceof Model) || (typeof t[0].model === 'function' && t[0].model.prototype instanceof Model)) {
if (typeof t[t.length - 2] === 'string') {
validateOrder(_.last(t));
}
} else {
validateOrder(_.last(t));
}
}
if (subQuery && (Array.isArray(t) && !(typeof t[0] === 'function' && t[0].prototype instanceof Model) && !(t[0] && typeof t[0].model === 'function' && t[0].model.prototype instanceof Model))) {
subQueryOrder.push(this.quote(t, model, false, '->'));
}
mainQueryOrder.push(this.quote(t, model, false, '->'));
}
} else {
var sql = this.quote(typeof options.order === 'string' ? new Utils.Literal(options.order) : options.order, model, false, '->');
if (subQuery) {
subQueryOrder.push(sql);
}
mainQueryOrder.push(sql);
}
return {mainQueryOrder, subQueryOrder};
},
selectFromTableFragment(options, model, attributes, tables, mainTableAs) {
let fragment = 'SELECT ' + attributes.join(', ') + ' FROM ' + tables;
if(mainTableAs) {
fragment += ' AS ' + mainTableAs;
}
return fragment;
}, },
/** /**
...@@ -1711,6 +1762,7 @@ const QueryGenerator = { ...@@ -1711,6 +1762,7 @@ const QueryGenerator = {
return items.length && items.filter(item => item && item.length).join(binding) || ''; return items.length && items.filter(item => item && item.length).join(binding) || '';
}, },
whereItemQuery(key, value, options) { whereItemQuery(key, value, options) {
options = options || {}; options = options || {};
let binding; let binding;
...@@ -2010,7 +2062,8 @@ const QueryGenerator = { ...@@ -2010,7 +2062,8 @@ const QueryGenerator = {
if (value.length > 2) { if (value.length > 2) {
value = [ value = [
value.slice(0, -1).join('.'), // join the tables by -> to match out internal namings
value.slice(0, -1).join('->'),
value[value.length - 1] value[value.length - 1]
]; ];
} }
...@@ -2069,7 +2122,8 @@ const QueryGenerator = { ...@@ -2069,7 +2122,8 @@ const QueryGenerator = {
if (key.length > 2) { if (key.length > 2) {
key = [ key = [
key.slice(0, -1).join('.'), // join the tables by -> to match out internal namings
key.slice(0, -1).join('->'),
key[key.length - 1] key[key.length - 1]
]; ];
} }
......
...@@ -750,7 +750,7 @@ const QueryGenerator = { ...@@ -750,7 +750,7 @@ const QueryGenerator = {
quoteIdentifier(identifier, force) { quoteIdentifier(identifier, force) {
if (identifier === '*') return identifier; if (identifier === '*') return identifier;
if (!force && this.options && this.options.quoteIdentifiers === false && identifier.indexOf('.') === -1) { // default is `true` if (!force && this.options && this.options.quoteIdentifiers === false && identifier.indexOf('.') === -1 && identifier.indexOf('->') === -1) { // default is `true`
// In Postgres, if tables or attributes are created double-quoted, // In Postgres, if tables or attributes are created double-quoted,
// they are also case sensitive. If they contain any uppercase // they are also case sensitive. If they contain any uppercase
// characters, they must always be double-quoted. This makes it // characters, they must always be double-quoted. This makes it
......
...@@ -45,11 +45,16 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() { ...@@ -45,11 +45,16 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() {
}]).then(function() { }]).then(function() {
return Task.findAll({ return Task.findAll({
include: [ include: [
{model: Project, include: [ {
{model: User, where: {username: 'leia'}} model: Project,
]} include: [
{model: User, where: {username: 'leia'}}
],
required: true,
}
] ]
}).then(function(tasks) { }).then(function(tasks) {
expect(tasks.length).to.be.equal(2); expect(tasks.length).to.be.equal(2);
expect(tasks[0].title).to.be.equal('fight empire'); expect(tasks[0].title).to.be.equal('fight empire');
expect(tasks[1].title).to.be.equal('stablish republic'); expect(tasks[1].title).to.be.equal('stablish republic');
...@@ -99,12 +104,16 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() { ...@@ -99,12 +104,16 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() {
}]).then(function() { }]).then(function() {
return Task.findAll({ return Task.findAll({
include: [ include: [
{model: Project, include: [ {
{model: User, where: { model: Project,
include: [
{model: User, where: {
username: 'leia', username: 'leia',
id: 1 id: 1
}} }}
]} ],
required: true,
}
] ]
}).then(function(tasks) { }).then(function(tasks) {
expect(tasks.length).to.be.equal(2); expect(tasks.length).to.be.equal(2);
...@@ -156,9 +165,13 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() { ...@@ -156,9 +165,13 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() {
}]).then(function() { }]).then(function() {
return User.findAll({ return User.findAll({
include: [ include: [
{model: Project, include: [ {
{model: Task, where: {title: 'fight empire'}} model: Project,
]} include: [
{model: Task, where: {title: 'fight empire'}}
],
required: true,
}
] ]
}).then(function(users) { }).then(function(users) {
expect(users.length).to.be.equal(1); expect(users.length).to.be.equal(1);
......
...@@ -858,4 +858,97 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -858,4 +858,97 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}); });
}); });
}); });
describe('nested includes', function () {
beforeEach(function () {
var Employee = this.sequelize.define('Employee', { 'name': DataTypes.STRING });
var Team = this.sequelize.define('Team', { 'name': DataTypes.STRING });
var Clearence = this.sequelize.define('Clearence', { 'level': DataTypes.INTEGER });
Team.Members = Team.hasMany(Employee, { as: 'members' });
Employee.Clearence = Employee.hasOne(Clearence, { as: 'clearence' });
Clearence.Employee = Clearence.belongsTo(Employee, { as: 'employee' });
this.Employee = Employee;
this.Team = Team;
this.Clearence = Clearence;
return this.sequelize.sync({ force: true }).then(function () {
return Promise.all([
Team.create({ name: 'TeamA' }),
Team.create({ name: 'TeamB' }),
Employee.create({ name: 'John' }),
Employee.create({ name: 'Jane' }),
Employee.create({ name: 'Josh' }),
Employee.create({ name: 'Jill' }),
Clearence.create({ level: 3 }),
Clearence.create({ level: 5 })
]).then(function (instances) {
return Promise.all([
instances[0].addMembers([instances[2], instances[3]]),
instances[1].addMembers([instances[4], instances[5]]),
instances[2].setClearence(instances[6]),
instances[3].setClearence(instances[7])
]);
});
});
});
it('should not ripple grandchild required to top level find when required of child is set to false', function () {
return this.Team.findAll({
include: [
{
association: this.Team.Members,
required: false,
include: [
{
association: this.Employee.Clearence,
required: true,
}
]
}
],
}).then(function (teams) {
expect(teams).to.have.length(2);
});
});
it('should not ripple grandchild required to top level find when required of child is not given (implicitly false)', function () {
return this.Team.findAll({
include: [
{
association: this.Team.Members,
include: [
{
association: this.Employee.Clearence,
required: true,
}
]
}
],
}).then(function (teams) {
expect(teams).to.have.length(2);
});
});
it('should ripple grandchild required to top level find when required of child is set to true as well', function () {
return this.Team.findAll({
include: [
{
association: this.Team.Members,
required: true,
include: [
{
association: this.Employee.Clearence,
required: true,
}
]
}
],
}).then(function (teams) {
expect(teams).to.have.length(1);
});
});
});
}); });
'use strict';
/* jshint -W110 */
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, Sequelize = require(__dirname + '/../../../lib/sequelize')
, util = require('util')
, _ = require('lodash')
, expectsql = Support.expectsql
, current = Support.sequelize
, sql = current.dialect.QueryGenerator;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
suite(Support.getTestDialectTeaser('SQL'), function() {
suite('generateJoin', function () {
var testsql = function (path, options, expectation) {
let name = `${path}, ${util.inspect(options, { depth: 10 })}`;
Sequelize.Model._conformOptions(options);
options = Sequelize.Model._validateIncludedElements(options);
let include = _.at(options, path)[0];
test(name, function () {
let join = sql.generateJoin(include,
{
options,
subQuery: options.subQuery === undefined ? options.limit && options.hasMultiAssociation : options.subQuery
}
);
return expectsql(`${join.join} ${join.body} ON ${join.condition}`, expectation);
});
};
var User = current.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id_user'
},
companyId: {
type: DataTypes.INTEGER,
field: 'company_id'
}
}, {
tableName: 'user'
});
var Task = current.define('Task', {
title: Sequelize.STRING,
userId: {
type: DataTypes.INTEGER,
field: 'user_id'
}
}, {
tableName: 'task'
});
var Company = current.define('Company', {
name: Sequelize.STRING,
ownerId: {
type: Sequelize.INTEGER,
field: 'owner_id'
},
public: {
type: Sequelize.BOOLEAN
}
}, {
tableName: 'company'
});
var Profession = current.define('Profession', {
name: Sequelize.STRING
}, {
tableName: 'profession'
});
User.Tasks = User.hasMany(Task, {as: 'Tasks', foreignKey: 'userId'});
User.Company = User.belongsTo(Company, {foreignKey: 'companyId'});
User.Profession = User.belongsTo(Profession, {foreignKey: 'professionId'});
Profession.Professionals = Profession.hasMany(User, {as: 'Professionals', foreignKey: 'professionId'});
Company.Employees = Company.hasMany(User, {as: 'Employees', foreignKey: 'companyId'});
Company.Owner = Company.belongsTo(User, {as: 'Owner', foreignKey: 'ownerId'});
/*
* BelongsTo
*/
testsql(
"include[0]",
{
model: User,
include: [
User.Company
]
},
{
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
}
);
testsql(
"include[0]",
{
model: User,
include: [
{
association: User.Company,
where: { public: true },
or: true
}
]
},
{
default: "INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id] OR [Company].[public] = true",
sqlite: "INNER JOIN `company` AS `Company` ON `User`.`company_id` = `Company`.`id` OR `Company`.`public` = 1",
mssql: "INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id] OR [Company].[public] = 1",
}
);
testsql(
"include[0].include[0]",
{
model: Profession,
include: [
{
association: Profession.Professionals,
limit: 3,
include: [
User.Company
]
}
]
},
{
default: "LEFT OUTER JOIN [company] AS [Professionals->Company] ON [Professionals].[company_id] = [Professionals->Company].[id]"
}
);
testsql(
"include[0]",
{
model: User,
subQuery: true,
include: [
User.Company
]
},
{
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]"
}
);
testsql(
"include[0]",
{
model: User,
subQuery: true,
include: [
{
association: User.Company, required: false, where: { name: 'ABC' }
}
]
},
{
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = 'ABC'",
mssql: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = N'ABC'"
}
);
testsql(
"include[0].include[0]",
{
subQuery: true,
model: User,
include: [
{
association: User.Company, include: [
Company.Owner
]
}
]
},
{
default: "LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]"
}
);
testsql(
"include[0].include[0].include[0]",
{
model: User,
subQuery: true,
include: [
{
association: User.Company,
include: [{
association: Company.Owner,
include: [
User.Profession
]
}]
}
]
},
{ default: "LEFT OUTER JOIN [profession] AS [Company->Owner->Profession] ON [Company->Owner].[professionId] = [Company->Owner->Profession].[id]" }
);
testsql(
"include[0].include[0]",
{
model: User,
subQuery: true,
include: [
{
association: User.Company,
required: true,
include: [
Company.Owner
]
}
]
},
{ default: "LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]" }
);
testsql(
"include[0]",
{
model: User,
subQuery: true,
include: [
{ association: User.Company, required: true }
]
},
{
default: "INNER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]"
}
);
// /*
// * HasMany
// */
testsql(
"include[0]",
{
model: User,
include: [
User.Tasks
]
},
{ default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]" }
);
testsql(
"include[0]",
{
model: User,
subQuery: true,
include: [
User.Tasks
]
},
{
// The primary key of the main model will be aliased because it's coming from a subquery that the :M join is not a part of
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]"
}
);
testsql(
"include[0]",
{
model: User,
include: [
{
association: User.Tasks, on: {
$or: [
{ '$User.id_user$': { $col: 'Tasks.user_id' } },
{ '$Tasks.user_id$': 2 }
]
}
}
]
}, { default: "LEFT OUTER JOIN [task] AS [Tasks] ON ([User].[id_user] = [Tasks].[user_id] OR [Tasks].[user_id] = 2)" }
);
testsql(
"include[0]",
{
model: User,
include: [
{
association: User.Tasks,
on: { 'user_id': { $col: 'User.alternative_id' } }
}
]
}, { default: "LEFT OUTER JOIN [task] AS [Tasks] ON [Tasks].[user_id] = [User].[alternative_id]" }
);
testsql(
"include[0].include[0]",
{
subQuery: true,
model: User,
include: [
{
association: User.Company,
include: [
{
association: Company.Owner,
on: {
$or: [
{ '$Company.owner_id$': { $col: 'Company.Owner.id_user'} },
{ '$Company.Owner.id_user$': 2 }
]
}
}
]
}
]
},
{
default: "LEFT OUTER JOIN [user] AS [Company->Owner] ON ([Company].[owner_id] = [Company->Owner].[id_user] OR [Company->Owner].[id_user] = 2)"
}
);
});
});
'use strict';
/* jshint -W110 */
var Support = require(__dirname + '/../support')
, DataTypes = require(__dirname + '/../../../lib/data-types')
, util = require('util')
, Sequelize = require(__dirname + '/../../../lib/sequelize')
, expectsql = Support.expectsql
, current = Support.sequelize
, sql = current.dialect.QueryGenerator;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
suite(Support.getTestDialectTeaser('SQL'), function() {
suite('joinIncludeQuery', function () {
var testsql = function (params, options, expectation) {
if (expectation === undefined) {
expectation = options;
options = undefined;
}
test(util.inspect(params, {depth: 10})+(options && ', '+util.inspect(options) || ''), function () {
return expectsql(sql.joinIncludeQuery(params, options), expectation);
});
};
var User = current.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id_user'
},
companyId: {
type: DataTypes.INTEGER,
field: 'company_id'
}
}, {
tableName: 'user'
});
var Task = current.define('Task', {
title: Sequelize.STRING,
userId: {
type: DataTypes.INTEGER,
field: 'user_id'
}
}, {
tableName: 'task'
});
var Company = current.define('Company', {
name: Sequelize.STRING,
ownerId: {
type: Sequelize.INTEGER,
field: 'owner_id'
},
public: {
type: Sequelize.BOOLEAN
}
}, {
tableName: 'company'
});
var Profession = current.define('Profession', {
name: Sequelize.STRING
}, {
tableName: 'profession'
});
User.Tasks = User.hasMany(Task, {as: 'Tasks', foreignKey: 'userId'});
User.Company = User.belongsTo(Company, {foreignKey: 'companyId'});
User.Profession = User.belongsTo(Profession, {foreignKey: 'professionId'});
Company.Employees = Company.hasMany(User, {as: 'Employees', foreignKey: 'companyId'});
Company.Owner = Company.belongsTo(User, {as: 'Owner', foreignKey: 'ownerId'});
/*
* BelongsTo
*/
testsql({
model: User,
subQuery: false,
include: Sequelize.Model._validateIncludedElements({
model: User,
include: [
User.Company
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
});
testsql({
model: User,
subQuery: false,
include: Sequelize.Model._validateIncludedElements({
model: User,
include: [
{association: User.Company, where: {public: true}, or: true}
]
}).include[0]
}, {
default: "INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id] OR [Company].[public] = true",
sqlite: "INNER JOIN `company` AS `Company` ON `User`.`company_id` = `Company`.`id` OR `Company`.`public` = 1",
mssql: "INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id] OR [Company].[public] = 1",
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
User.Company
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
});
testsql({
model: User,
subQuery: true,
groupedLimit: {},
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
User.Company
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, required: false, where: {
name: 'ABC'
}},
User.Tasks
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = 'ABC'",
mssql: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = N'ABC'"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, include: [
Company.Owner
]}
]
}).include[0].include[0]
}, {
default: "LEFT OUTER JOIN [user] AS [Company.Owner] ON [Company].[owner_id] = [Company.Owner].[id_user]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, include: [
{association: Company.Owner, include: [
User.Profession
]}
]}
]
}).include[0].include[0].include[0]
}, {
default: "LEFT OUTER JOIN [profession] AS [Company.Owner.Profession] ON [Company.Owner].[professionId] = [Company.Owner.Profession].[id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, required: true, include: [
Company.Owner
]},
User.Tasks
]
}).include[0].include[0]
}, {
default: "LEFT OUTER JOIN [user] AS [Company.Owner] ON [Company.ownerId] = [Company.Owner].[id_user]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
{association: User.Company, required: true}
]
}).include[0]
}, {
default: "INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]"
});
/*
* HasMany
*/
testsql({
model: User,
subQuery: false,
include: Sequelize.Model._validateIncludedElements({
model: User,
include: [
User.Tasks
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
User.Tasks
]
}).include[0]
}, {
// The primary key of the main model will be aliased because it's coming from a subquery that the :M join is not a part of
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]"
});
testsql({
model: User,
subQuery: true,
include: Sequelize.Model._validateIncludedElements({
limit: 3,
model: User,
include: [
User.Tasks
]
}).include[0]
}, {
// The primary key of the main model will be aliased because it's coming from a subquery that the :M join is not a part of
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]"
});
testsql({
model: User,
subQuery: false,
include: Sequelize.Model._validateIncludedElements({
model: User,
include: [
{association: User.Tasks, on: {
$or: [
{'$User.id_user$': {$col: 'Tasks.user_id'}},
{'$Tasks.user_id$': 2}
]
}}
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [task] AS [Tasks] ON ([User].[id_user] = [Tasks].[user_id] OR [Tasks].[user_id] = 2)"
});
testsql({
model: User,
subQuery: false,
include: Sequelize.Model._validateIncludedElements({
model: User,
include: [
{association: User.Tasks, on: {'user_id': {$col: 'User.alternative_id'}}}
]
}).include[0]
}, {
default: "LEFT OUTER JOIN [task] AS [Tasks] ON [Tasks].[user_id] = [User].[alternative_id]"
});
});
});
...@@ -290,12 +290,12 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -290,12 +290,12 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
] ]
} }
}, { }, {
default: 'SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title], [POSTS.COMMENTS].[id] AS [POSTS.COMMENTS.id], [POSTS.COMMENTS].[title] AS [POSTS.COMMENTS.title] FROM ('+ default: 'SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title], [POSTS->COMMENTS].[id] AS [POSTS.COMMENTS.id], [POSTS->COMMENTS].[title] AS [POSTS.COMMENTS.title] FROM ('+
[ [
'(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')', '(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')',
'(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 5 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')' '(SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 5 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')'
].join(current.dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ') ].join(current.dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ')
+') AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id] = [POSTS].[user_id] LEFT OUTER JOIN [comment] AS [POSTS.COMMENTS] ON [POSTS].[id] = [POSTS.COMMENTS].[post_id];' +') AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id] = [POSTS].[user_id] LEFT OUTER JOIN [comment] AS [POSTS->COMMENTS] ON [POSTS].[id] = [POSTS->COMMENTS].[post_id];'
}); });
})(); })();
...@@ -430,8 +430,8 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -430,8 +430,8 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
}).include, }).include,
model: User model: User
}, User), { }, User), {
default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title], [Posts.Comments].[id] AS [Posts.Comments.id], [Posts.Comments].[title] AS [Posts.Comments.title], [Posts.Comments].[createdAt] AS [Posts.Comments.createdAt], [Posts.Comments].[updatedAt] AS [Posts.Comments.updatedAt], [Posts.Comments].[post_id] AS [Posts.Comments.post_id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id] LEFT OUTER JOIN [Comment] AS [Posts.Comments] ON [Posts].[id] = [Posts.Comments].[post_id];', default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title], [Posts->Comments].[id] AS [Posts.Comments.id], [Posts->Comments].[title] AS [Posts.Comments.title], [Posts->Comments].[createdAt] AS [Posts.Comments.createdAt], [Posts->Comments].[updatedAt] AS [Posts.Comments.updatedAt], [Posts->Comments].[post_id] AS [Posts.Comments.post_id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id] LEFT OUTER JOIN [Comment] AS [Posts->Comments] ON [Posts].[id] = [Posts->Comments].[post_id];',
postgres: 'SELECT User.name, User.age, Posts.id AS "Posts.id", Posts.title AS "Posts.title", "Posts.Comments".id AS "Posts.Comments.id", "Posts.Comments".title AS "Posts.Comments.title", "Posts.Comments".createdAt AS "Posts.Comments.createdAt", "Posts.Comments".updatedAt AS "Posts.Comments.updatedAt", "Posts.Comments".post_id AS "Posts.Comments.post_id" FROM User AS User LEFT OUTER JOIN Post AS Posts ON User.id = Posts.user_id LEFT OUTER JOIN Comment AS "Posts.Comments" ON Posts.id = "Posts.Comments".post_id;' postgres: 'SELECT User.name, User.age, Posts.id AS "Posts.id", Posts.title AS "Posts.title", "Posts->Comments".id AS "Posts.Comments.id", "Posts->Comments".title AS "Posts.Comments.title", "Posts->Comments".createdAt AS "Posts.Comments.createdAt", "Posts->Comments".updatedAt AS "Posts.Comments.updatedAt", "Posts->Comments".post_id AS "Posts.Comments.post_id" FROM User AS User LEFT OUTER JOIN Post AS Posts ON User.id = Posts.user_id LEFT OUTER JOIN Comment AS "Posts->Comments" ON Posts.id = "Posts->Comments".post_id;'
}); });
}); });
......
...@@ -377,7 +377,7 @@ suite(Support.getTestDialectTeaser('SQL'), function() { ...@@ -377,7 +377,7 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
testsql('$offer.organization.id$', { testsql('$offer.organization.id$', {
$col: 'offer.user.organizationId' $col: 'offer.user.organizationId'
}, { }, {
default: '[offer.organization].[id] = [offer.user].[organizationId]' default: '[offer->organization].[id] = [offer->user].[organizationId]'
}); });
}); });
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!