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

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,56 +749,285 @@ const QueryGenerator = { ...@@ -748,56 +749,285 @@ 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);
}
}
}
attributes.main = this.escapeAttributes(attributes.main, options, mainTable.as);
attributes.main = attributes.main || (options.include ? [`${mainTable.as}.*`] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery
if (subQuery || options.groupedLimit) {
// We need primary keys
attributes.subQuery = attributes.main;
attributes.main = [(mainTable.as || mainTable.quotedName) + '.*'];
}
if (options.include) {
for (const include of options.include) {
if (include.separate) {
continue;
}
const joinQueries = this.generateInclude(include, { externalAs: mainTable.as, internalAs: mainTable.as }, topLevelInfo);
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery);
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 (subQuery) {
subQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.subQuery, mainTable.quotedName, mainTable.as));
subQueryItems.push(subJoinQueries.join(''));
} else {
if (options.groupedLimit) {
if (!mainTable.as) {
mainTable.as = mainTable.quotedName;
}
const where = Object.assign({}, options.where);
let groupedLimitOrder
, whereKey
, include
, groupedTableName = mainTable.as;
if (typeof options.groupedLimit.on === 'string') {
whereKey = options.groupedLimit.on;
} else if (options.groupedLimit.on instanceof HasMany) {
whereKey = options.groupedLimit.on.foreignKeyField;
} }
// Escape attributes if (options.groupedLimit.on instanceof BelongsToMany) {
mainAttributes = mainAttributes && mainAttributes.map(attr => { // BTM includes needs to join the through table on to check ID
groupedTableName = options.groupedLimit.on.manyFromSource.as;
const groupedLimitOptions = Model._validateIncludedElements({
include: [{
association: options.groupedLimit.on.manyFromSource,
duplicating: false, // The UNION'ed query may contain duplicates, but each sub-query cannot
required: true,
where: {
'$$PLACEHOLDER$$': true
}
}],
model
});
// Make sure attributes from the join table are mapped back to models
options.hasJoin = true;
options.hasMultiAssociation = true;
options.includeMap = Object.assign(groupedLimitOptions.includeMap, options.includeMap);
options.includeNames = groupedLimitOptions.includeNames.concat(options.includeNames || []);
include = groupedLimitOptions.include;
if (Array.isArray(options.order)) {
// We need to make sure the order by attributes are available to the parent query
options.order.forEach((order, i) => {
if (Array.isArray(order)) {
order = order[0];
}
let alias = `subquery_order_${i}`;
options.attributes.push([order, alias]);
// We don't want to prepend model name when we alias the attributes, so quote them here
alias = this.sequelize.literal(this.quote(alias));
if (Array.isArray(options.order[i])) {
options.order[i][0] = alias;
} else {
options.order[i] = alias;
}
});
groupedLimitOrder = options.order;
}
} else {
// Ordering is handled by the subqueries, so ordering the UNION'ed result is not needed
groupedLimitOrder = options.order;
delete options.order;
where.$$PLACEHOLDER$$ = true;
}
// 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
const baseQuery = '(' + this.selectQuery(
tableName,
{
attributes: options.attributes,
limit: options.groupedLimit.limit,
order: groupedLimitOrder,
where,
include,
model
},
model
).replace(/;$/, '') + ')';
const placeHolder = this.whereItemQuery('$$PLACEHOLDER$$', true, { model });
const splicePos = baseQuery.indexOf(placeHolder);
mainQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.main, '(' +
options.groupedLimit.values.map(value => {
let groupWhere;
if (whereKey) {
groupWhere = {
[whereKey]: value
};
}
if (include) {
groupWhere = {
[options.groupedLimit.on.otherKey]: value
};
}
return Utils.spliceStr(baseQuery, splicePos, placeHolder.length, this.getWhereConditions(groupWhere, groupedTableName));
}).join(
this._dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION '
)
+ ')', mainTable.as));
} else {
mainQueryItems.push(this.selectFromTableFragment(options, mainTable.model, attributes.main, mainTable.quotedName, mainTable.as));
}
mainQueryItems.push(mainJoinQueries.join(''));
}
// Add WHERE to sub or main query
if (options.hasOwnProperty('where') && !options.groupedLimit) {
options.where = this.getWhereConditions(options.where, mainTable.as || tableName, model, options);
if (options.where) {
if (subQuery) {
subQueryItems.push(' WHERE ' + options.where);
} else {
mainQueryItems.push(' WHERE ' + options.where);
// Walk the main query to update all selects
_.each(mainQueryItems, (value, key) => {
if (value.match(/^SELECT/)) {
mainQueryItems[key] = this.selectFromTableFragment(options, model, attributes.main, mainTable.quotedName, mainTable.as, options.where);
}
});
}
}
}
// Add GROUP BY to sub or main query
if (options.group) {
options.group = Array.isArray(options.group) ? options.group.map(t => this.quote(t, model)).join(', ') : options.group;
if (subQuery) {
subQueryItems.push(' GROUP BY ' + options.group);
} else {
mainQueryItems.push(' GROUP BY ' + options.group);
}
}
// Add HAVING to sub or main query
if (options.hasOwnProperty('having')) {
options.having = this.getWhereConditions(options.having, tableName, model, options, false);
if (subQuery) {
subQueryItems.push(' HAVING ' + options.having);
} else {
mainQueryItems.push(' HAVING ' + options.having);
}
}
// Add ORDER to sub or main query
if (options.order) {
const orders = this.getQueryOrders(options, model, subQuery);
if (orders.mainQueryOrder.length) {
mainQueryItems.push(' ORDER BY ' + orders.mainQueryOrder.join(', '));
}
if (orders.subQueryOrder.length) {
subQueryItems.push(' ORDER BY ' + orders.subQueryOrder.join(', '));
}
}
// Add LIMIT, OFFSET to sub or main query
const limitOrder = this.addLimitAndOffset(options, mainTable.model);
if (limitOrder && !options.groupedLimit) {
if (subQuery) {
subQueryItems.push(limitOrder);
} else {
mainQueryItems.push(limitOrder);
}
}
if (subQuery) {
query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) AS ${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`;
} else {
query = mainQueryItems.join('');
}
if (options.lock && this._dialect.supports.lock) {
let lock = options.lock;
if (typeof options.lock === 'object') {
lock = options.lock.level;
}
if (this._dialect.supports.lockKey && (lock === 'KEY SHARE' || lock === 'NO KEY UPDATE')) {
query += ' FOR ' + lock;
} else if (lock === 'SHARE') {
query += ' ' + this._dialect.supports.forShare;
} else {
query += ' FOR UPDATE';
}
if (this._dialect.supports.lockOf && options.lock.of && options.lock.of.prototype instanceof Model) {
query += ' OF ' + this.quoteTable(options.lock.of.name);
}
}
return `${query};`;
},
escapeAttributes(attributes, options, mainTableAs) {
return attributes && attributes.map(attr => {
let addTable = true; let addTable = true;
if (attr._isSequelizeMethod) { if (attr._isSequelizeMethod) {
return this.handleSequelizeMethod(attr); return this.handleSequelizeMethod(attr);
} }
if (Array.isArray(attr)) { if (Array.isArray(attr)) {
if (attr.length !== 2) { if (attr.length !== 2) {
throw new Error(JSON.stringify(attr) + ' is not a valid attribute definition. Please use the following format: [\'attribute definition\', \'alias\']'); throw new Error(JSON.stringify(attr) + ' is not a valid attribute definition. Please use the following format: [\'attribute definition\', \'alias\']');
...@@ -814,49 +1044,43 @@ const QueryGenerator = { ...@@ -814,49 +1044,43 @@ const QueryGenerator = {
} else { } else {
attr = attr.indexOf(Utils.TICK_CHAR) < 0 && attr.indexOf('"') < 0 ? this.quoteIdentifiers(attr) : attr; attr = attr.indexOf(Utils.TICK_CHAR) < 0 && attr.indexOf('"') < 0 ? this.quoteIdentifiers(attr) : attr;
} }
if (options.include && attr.indexOf('.') === -1 && addTable) { if (options.include && attr.indexOf('.') === -1 && addTable) {
attr = mainTableAs + '.' + attr; attr = mainTableAs + '.' + attr;
} }
return attr; return attr;
}); });
},
// If no attributes specified, use * generateInclude(include, parentTableName, topLevelInfo) {
mainAttributes = mainAttributes || (options.include ? [mainTableAs + '.*'] : ['*']);
// If subquery, we ad the mainAttributes to the subQuery and set the mainAttributes to select * from subquery
if (subQuery || options.groupedLimit) {
// We need primary keys
subQueryAttributes = mainAttributes;
mainAttributes = [(mainTableAs || table) + '.*'];
}
if (options.include) {
const generateJoinQueries = (include, parentTable) => {
const association = include.association; 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 = { const joinQueries = {
mainQuery: [], mainQuery: [],
subQuery: [] subQuery: [],
}; };
let as = include.as; const mainChildIncludes = [];
let joinQueryItem = ''; const subChildIncludes = [];
let attributes; let requiredMismatch = false;
let targetWhere; let includeAs = {
internalAs: include.as,
externalAs: include.as
};
let attributes = {
main: [],
subQuery: [],
};
let joinQuery;
whereOptions.keysEscaped = true; topLevelInfo.options.keysEscaped = true;
if (tableName !== parentTable && mainTableAs !== parentTable) { if (topLevelInfo.names.name !== parentTableName.externalAs && topLevelInfo.names.as !== parentTableName.externalAs) {
as = parentTable + '.' + include.as; includeAs.internalAs = `${parentTableName.internalAs}->${include.as}`;
includeAs.externalAs = `${parentTableName.externalAs}.${include.as}`;
} }
// includeIgnoreAttributes is used by aggregate functions // includeIgnoreAttributes is used by aggregate functions
if (options.includeIgnoreAttributes !== false) { if (topLevelInfo.options.includeIgnoreAttributes !== false) {
attributes = include.attributes.map(attr => { let includeAttributes = include.attributes.map(attr => {
let attrAs = attr; let attrAs = attr;
let verbatim = false; let verbatim = false;
...@@ -887,176 +1111,35 @@ const QueryGenerator = { ...@@ -887,176 +1111,35 @@ const QueryGenerator = {
if (verbatim === true) { if (verbatim === true) {
prefix = attr; prefix = attr;
} else { } else {
prefix = this.quoteIdentifier(as) + '.' + this.quoteIdentifier(attr); prefix = `${this.quoteIdentifier(includeAs.internalAs)}.${this.quoteIdentifier(attr)}`;
} }
return prefix + ' AS ' + this.quoteIdentifier(as + '.' + attrAs, true); return `${prefix} AS ${this.quoteIdentifier(`${includeAs.externalAs}.${attrAs}`, true)}`;
}); });
if (include.subQuery && subQuery) { if (include.subQuery && topLevelInfo.subQuery) {
subQueryAttributes = subQueryAttributes.concat(attributes); for (let attr of includeAttributes) {
} else { attributes.subQuery.push(attr);
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 { } else {
sourceJoinOn = this.quoteTable(tableSource) + '.' + this.quoteIdentifier(attrSource) + ' = '; for (let attr of includeAttributes) {
attributes.main.push(attr);
} }
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; //through
if (include.through) {
joinQuery = this.generateThroughJoin(include, includeAs, parentTableName.internalAs, topLevelInfo);
} else { } else {
// Generate join SQL for left side of through if (topLevelInfo.subQuery && include.subQueryFilter) {
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 = {}; const associationWhere = {};
associationWhere[association.identifierField] = { associationWhere[association.identifierField] = {
$raw: this.quoteTable(parentTable) + '.' + this.quoteIdentifier(association.source.primaryKeyField) $raw: `${this.quoteTable(parentTableName.internalAs)}.${this.quoteIdentifier(association.source.primaryKeyField)}`
}; };
if (!options.where) options.where = {}; if (!topLevelInfo.options.where) {
topLevelInfo.options.where = {};
}
// Creating the as-is where for the subQuery, checks that the required association exists // Creating the as-is where for the subQuery, checks that the required association exists
const $query = this.selectQuery(include.model.getTableName(), { const $query = this.selectQuery(include.model.getTableName(), {
...@@ -1077,269 +1160,319 @@ const QueryGenerator = { ...@@ -1077,269 +1160,319 @@ const QueryGenerator = {
'IS NOT NULL' 'IS NOT NULL'
].join(' ')); ].join(' '));
if (Utils._.isPlainObject(options.where)) { if (Utils._.isPlainObject(topLevelInfo.options.where)) {
options.where['__' + as] = subQueryWhere; topLevelInfo.options.where['__' + includeAs] = subQueryWhere;
} else { } else {
options.where = { $and: [options.where, subQueryWhere] }; topLevelInfo.options.where = { $and: [topLevelInfo.options.where, subQueryWhere] };
}
} }
joinQuery = this.generateJoin(include, topLevelInfo);
} }
joinQueryItem = ' ' + this.joinIncludeQuery({ // handle possible new attributes created in join
model: mainModel, if (joinQuery.attributes.main.length > 0) {
subQuery: options.subQuery, attributes.main = attributes.main.concat(joinQuery.attributes.main);
include,
groupedLimit: options.groupedLimit
});
} }
if (include.subQuery && subQuery) { if (joinQuery.attributes.subQuery.length > 0) {
joinQueries.subQuery.push(joinQueryItem); attributes.subQuery = attributes.subQuery.concat(joinQuery.attributes.subQuery);
} else {
joinQueries.mainQuery.push(joinQueryItem);
} }
if (include.include) { if (include.include) {
for (const childInclude of include.include) { for (const childInclude of include.include) {
if (childInclude.separate || childInclude._pseudo) { if (childInclude.separate || childInclude._pseudo) {
continue; continue;
} }
const childJoinQueries = generateJoinQueries(childInclude, as); const childJoinQueries = this.generateInclude(childInclude, includeAs, topLevelInfo);
if (childInclude.subQuery && subQuery) { if (include.required === false && childInclude.required === true) {
joinQueries.subQuery = joinQueries.subQuery.concat(childJoinQueries.subQuery); 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) { if (childJoinQueries.mainQuery) {
joinQueries.mainQuery = joinQueries.mainQuery.concat(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 joinQueries;
};
// Loop through includes and generate subqueries
for (const include of options.include) {
if (include.separate) {
continue;
} }
const joinQueries = generateJoinQueries(include, mainTableAs); if (include.subQuery && topLevelInfo.subQuery) {
if (requiredMismatch && subChildIncludes.length > 0) {
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery); joinQueries.subQuery.push(` ${joinQuery.join} ( ${joinQuery.body}${subChildIncludes.join('')} ) ON ${joinQuery.condition}`);
mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery); } 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(''));
// If using subQuery select defined subQuery attributes and join subJoinQueries
if (subQuery) {
subQueryItems.push(this.selectFromTableFragment(options, model, subQueryAttributes, table, mainTableAs));
subQueryItems.push(subJoinQueries.join(''));
// Else do it the reguar way
} else { } else {
if (options.groupedLimit) { if (requiredMismatch && mainChildIncludes.length > 0) {
if (!mainTableAs) { joinQueries.mainQuery.push(` ${joinQuery.join} ( ${joinQuery.body}${mainChildIncludes.join('')} ) ON ${joinQuery.condition}`);
mainTableAs = table; } 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(''));
} }
const where = Object.assign({}, options.where); return {
let groupedLimitOrder mainQuery: joinQueries.mainQuery.join(''),
, whereKey subQuery: joinQueries.subQuery.join(''),
, include attributes: attributes,
, groupedTableName = mainTableAs; };
},
if (typeof options.groupedLimit.on === 'string') { generateJoin(include, topLevelInfo) {
whereKey = options.groupedLimit.on; const association = include.association;
} else if (options.groupedLimit.on instanceof HasMany) { const parent = include.parent;
whereKey = options.groupedLimit.on.foreignKeyField; const parentIsTop = !!parent && !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
} let $parent;
let joinWhere;
/* Attributes for the left side */
const left = association.source;
const attrLeft = association instanceof BelongsTo ?
association.identifier :
left.primaryKeyAttribute;
const fieldLeft = association instanceof BelongsTo ?
association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field;
let asLeft;
/* Attributes for the right side */
const right = include.model;
const tableRight = right.getTableName();
const fieldRight = association instanceof BelongsTo ?
right.rawAttributes[association.targetIdentifier || right.primaryKeyAttribute].field :
association.identifierField;
let asRight = include.as;
if (options.groupedLimit.on instanceof BelongsToMany) { while (($parent = ($parent && $parent.parent || include.parent)) && $parent.association) {
// BTM includes needs to join the through table on to check ID if (asLeft) {
groupedTableName = options.groupedLimit.on.manyFromSource.as; asLeft = `${$parent.as}->${asLeft}`;
const groupedLimitOptions = Model._validateIncludedElements({ } else {
include: [{ asLeft = $parent.as;
association: options.groupedLimit.on.manyFromSource, }
duplicating: false, // The UNION'ed query may contain duplicates, but each sub-query cannot
required: true,
where: {
'$$PLACEHOLDER$$': true
} }
}],
model
});
// Make sure attributes from the join table are mapped back to models if (!asLeft) asLeft = parent.as || parent.model.name;
options.hasJoin = true; else asRight = `${asLeft}->${asRight}`;
options.hasMultiAssociation = true;
options.includeMap = Object.assign(groupedLimitOptions.includeMap, options.includeMap);
options.includeNames = groupedLimitOptions.includeNames.concat(options.includeNames || []);
include = groupedLimitOptions.include;
if (Array.isArray(options.order)) { let joinOn = `${this.quoteTable(asLeft)}.${this.quoteIdentifier(fieldLeft)}`;
// We need to make sure the order by attributes are available to the parent query
options.order.forEach((order, i) => { if ((topLevelInfo.options.groupedLimit && parentIsTop) || (topLevelInfo.subQuery && include.parent.subQuery && !include.subQuery)) {
if (Array.isArray(order)) { if (parentIsTop) {
order = order[0]; // The main model attributes is not aliased to a prefix
joinOn = `${this.quoteTable(parent.as || parent.model.name)}.${this.quoteIdentifier(attrLeft)}`;
} else {
joinOn = this.quoteIdentifier(`${asLeft}.${attrLeft}`);
}
} }
let alias = `subquery_order_${i}`; joinOn += ` = ${this.quoteIdentifier(asRight)}.${this.quoteIdentifier(fieldRight)}`;
options.attributes.push([order, alias]);
// We don't want to prepend model name when we alias the attributes, so quote them here if (include.on) {
alias = this.sequelize.literal(this.quote(alias)); joinOn = this.whereItemsQuery(include.on, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});
}
if (Array.isArray(options.order[i])) { if (include.where) {
options.order[i][0] = alias; joinWhere = this.whereItemsQuery(include.where, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});
if (joinWhere) {
if (include.or) {
joinOn += ` OR ${joinWhere}`;
} else { } else {
options.order[i] = alias; joinOn += ` AND ${joinWhere}`;
} }
});
groupedLimitOrder = options.order;
} }
} else {
// Ordering is handled by the subqueries, so ordering the UNION'ed result is not needed
groupedLimitOrder = options.order;
delete options.order;
where.$$PLACEHOLDER$$ = true;
} }
// Caching the base query and splicing the where part into it is consistently > twice return {
// as fast than generating from scratch each time for values.length >= 5 join: include.required ? 'INNER JOIN' : 'LEFT OUTER JOIN',
const baseQuery = '('+this.selectQuery( body: this.quoteTable(tableRight, asRight),
tableName, condition: joinOn,
{ attributes: {
attributes: options.attributes, main: [],
limit: options.groupedLimit.limit, subQuery: [],
order: groupedLimitOrder, }
where, };
include,
model
}, },
model
).replace(/;$/, '')+')';
const placeHolder = this.whereItemQuery('$$PLACEHOLDER$$', true, { model });
const splicePos = baseQuery.indexOf(placeHolder);
mainQueryItems.push(this.selectFromTableFragment(options, model, mainAttributes, '('+ generateThroughJoin(include, includeAs, parentTableName, topLevelInfo) {
options.groupedLimit.values.map(value => { const through = include.through;
let groupWhere; const throughTable = through.model.getTableName();
if (whereKey) { const throughAs = `${includeAs.internalAs}->${through.as}`;
groupWhere = { const externalThroughAs = `${includeAs.externalAs}.${through.as}`;
[whereKey]: value const throughAttributes = through.attributes.map(attr =>
}; this.quoteIdentifier(throughAs) + '.' + this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)
} + ' AS '
if (include) { + this.quoteIdentifier(externalThroughAs + '.' + (Array.isArray(attr) ? attr[1] : attr))
groupWhere = { );
[options.groupedLimit.on.otherKey]: value 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;
return Utils.spliceStr(baseQuery, splicePos, placeHolder.length, this.getWhereConditions(groupWhere, groupedTableName)); if (topLevelInfo.options.includeIgnoreAttributes !== false) {
}).join( // 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)
this._dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION ' for (let attr of throughAttributes) {
) attributes.main.push(attr);
+')', mainTableAs));
} else {
mainQueryItems.push(this.selectFromTableFragment(options, model, mainAttributes, table, mainTableAs));
} }
mainQueryItems.push(mainJoinQueries.join(''));
} }
// Add WHERE to sub or main query // Figure out if we need to use field or attribute
if (options.hasOwnProperty('where') && !options.groupedLimit) { if (!topLevelInfo.subQuery) {
options.where = this.getWhereConditions(options.where, mainTableAs || tableName, model, options); attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
if (options.where) {
if (subQuery) {
subQueryItems.push(' WHERE ' + options.where);
} else {
mainQueryItems.push(' WHERE ' + options.where);
// Walk the main query to update all selects
_.each(mainQueryItems, (value, key) => {
if(value.match(/^SELECT/)) {
mainQueryItems[key] = this.selectFromTableFragment(options, model, mainAttributes, table, mainTableAs, options.where);
}
});
}
} }
if (topLevelInfo.subQuery && !include.subQuery && !include.parent.subQuery && include.parent.model !== topLevelInfo.options.mainModel) {
attrSource = association.source.rawAttributes[primaryKeysSource[0]].field;
} }
// Add GROUP BY to sub or main query // Filter statement for left side of through
if (options.group) { // Used by both join and subquery where
options.group = Array.isArray(options.group) ? options.group.map(t => this.quote(t, model)).join(', ') : options.group; // If parent include was in a subquery need to join on the aliased attribute
if (subQuery) { if (topLevelInfo.subQuery && !include.subQuery && include.parent.subQuery && !parentIsTop) {
subQueryItems.push(' GROUP BY ' + options.group); sourceJoinOn = `${this.quoteIdentifier(`${tableSource}.${attrSource}`)} = `;
} else { } else {
mainQueryItems.push(' GROUP BY ' + options.group); 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);
} }
// Add HAVING to sub or main query if (this._dialect.supports.joinTableDependent) {
if (options.hasOwnProperty('having')) { // Generate a wrapped join so that the through table join can be dependent on the target join
options.having = this.getWhereConditions(options.having, tableName, model, options, false); joinBody = `( ${this.quoteTable(throughTable, throughAs)} INNER JOIN ${this.quoteTable(include.model.getTableName(), includeAs.internalAs)} ON ${targetJoinOn}`;
if (subQuery) { if (throughWhere) {
subQueryItems.push(' HAVING ' + options.having); joinBody += ` AND ${throughWhere}`;
}
joinBody += ')';
joinCondition = sourceJoinOn;
} else { } else {
mainQueryItems.push(' HAVING ' + options.having); // 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}`;
} }
} }
// Add ORDER to sub or main query
if (options.order) {
const orders = this.getQueryOrders(options, model, subQuery);
if (orders.mainQueryOrder.length) { if (include.where || include.through.where) {
mainQueryItems.push(' ORDER BY ' + orders.mainQueryOrder.join(', ')); 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 (orders.subQueryOrder.length) {
subQueryItems.push(' ORDER BY ' + orders.subQueryOrder.join(', '));
} }
if (topLevelInfo.subQuery && include.required) {
if (!topLevelInfo.options.where) {
topLevelInfo.options.where = {};
} }
let parent = include;
let child = include;
let nestedIncludes = [];
let query;
// Add LIMIT, OFFSET to sub or main query while (parent = parent.parent) {
const limitOrder = this.addLimitAndOffset(options, model); nestedIncludes = [_.extend({}, child, { include: nestedIncludes })];
if (limitOrder && !options.groupedLimit) { child = parent;
if (subQuery) {
subQueryItems.push(limitOrder);
} else {
mainQueryItems.push(limitOrder);
}
} }
// If using subQuery, select attributes from wrapped subQuery and join out join tables const topInclude = nestedIncludes[0];
if (subQuery) { const topParent = topInclude.parent;
query = 'SELECT ' + mainAttributes.join(', ') + ' FROM (';
query += subQueryItems.join('');
query += ') AS ' + mainTableAs;
query += mainJoinQueries.join('');
query += mainQueryItems.join('');
} else {
query = mainQueryItems.join('');
}
if (options.lock && this._dialect.supports.lock) { if (topInclude.through && Object(topInclude.through.model) === topInclude.through.model) {
let lock = options.lock; query = this.selectQuery(topInclude.through.model.getTableName(), {
if (typeof options.lock === 'object') { attributes: [topInclude.through.model.primaryKeyField],
lock = options.lock.level; include: Model._validateIncludedElements({
} model: topInclude.through.model,
if (this._dialect.supports.lockKey && (lock === 'KEY SHARE' || lock === 'NO KEY UPDATE')) { include: [{
query += ' FOR ' + lock; association: topInclude.association.toTarget,
} else if (lock === 'SHARE') { required: true
query += ' ' + this._dialect.supports.forShare; }]
}).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 {
query += ' FOR UPDATE'; 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);
} }
if (this._dialect.supports.lockOf && options.lock.of && options.lock.of.prototype instanceof Model) { topLevelInfo.options.where['__' + throughAs] = this.sequelize.asIs([
query += ' OF ' + this.quoteTable(options.lock.of.name); '(',
query.replace(/\;$/, ''),
')',
'IS NOT NULL'
].join(' '));
} }
} }
query += ';'; return {
join: joinType,
return query; body: joinBody,
condition: joinCondition,
attributes: attributes,
};
}, },
getQueryOrders(options, model, subQuery) { getQueryOrders(options, model, subQuery) {
...@@ -1376,13 +1509,13 @@ const QueryGenerator = { ...@@ -1376,13 +1509,13 @@ const QueryGenerator = {
} }
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))) { 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)); subQueryOrder.push(this.quote(t, model, false, '->'));
} }
mainQueryOrder.push(this.quote(t, model)); mainQueryOrder.push(this.quote(t, model, false, '->'));
} }
} else { } else {
var sql = this.quote(typeof options.order === 'string' ? new Utils.Literal(options.order) : options.order, model); var sql = this.quote(typeof options.order === 'string' ? new Utils.Literal(options.order) : options.order, model, false, '->');
if (subQuery) { if (subQuery) {
subQueryOrder.push(sql); subQueryOrder.push(sql);
} }
...@@ -1402,88 +1535,6 @@ const QueryGenerator = { ...@@ -1402,88 +1535,6 @@ const QueryGenerator = {
return fragment; return fragment;
}, },
joinIncludeQuery(options) {
const subQuery = options.subQuery;
const include = options.include;
const association = include.association;
const parent = include.parent;
const parentIsTop = !include.parent.association && include.parent.model.name === options.model.name;
const joinType = include.required ? 'INNER JOIN ' : 'LEFT OUTER JOIN ';
let $parent;
let joinWhere;
/* Attributes for the left side */
const left = association.source;
const attrLeft = association instanceof BelongsTo ?
association.identifier :
left.primaryKeyAttribute;
const fieldLeft = association instanceof BelongsTo ?
association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field;
let asLeft;
/* Attributes for the right side */
const right = include.model;
const tableRight = right.getTableName();
const fieldRight = association instanceof BelongsTo ?
right.rawAttributes[association.targetIdentifier || right.primaryKeyAttribute].field :
association.identifierField;
let asRight = include.as;
while (($parent = ($parent && $parent.parent || include.parent)) && $parent.association) {
if (asLeft) {
asLeft = [$parent.as, asLeft].join('.');
} else {
asLeft = $parent.as;
}
}
if (!asLeft) asLeft = parent.as || parent.model.name;
else asRight = [asLeft, asRight].join('.');
let joinOn = [
this.quoteTable(asLeft),
this.quoteIdentifier(fieldLeft)
].join('.');
if ((options.groupedLimit && parentIsTop) || (subQuery && include.parent.subQuery && !include.subQuery)) {
if (parentIsTop) {
// The main model attributes is not aliased to a prefix
joinOn = [
this.quoteTable(parent.as || parent.model.name),
this.quoteIdentifier(attrLeft)
].join('.');
} else {
joinOn = this.quoteIdentifier(asLeft + '.' + attrLeft);
}
}
joinOn += ' = ' + this.quoteIdentifier(asRight) + '.' + this.quoteIdentifier(fieldRight);
if (include.on) {
joinOn = this.whereItemsQuery(include.on, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});
}
if (include.where) {
joinWhere = this.whereItemsQuery(include.where, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});
if (joinWhere) {
if (include.or) {
joinOn += ' OR ' + joinWhere;
} else {
joinOn += ' AND ' + joinWhere;
}
}
}
return joinType + this.quoteTable(tableRight, asRight) + ' ON ' + joinOn;
},
/** /**
* Returns a query that starts a transaction. * Returns a query that starts a transaction.
* *
...@@ -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: Project,
include: [
{model: User, where: {username: 'leia'}} {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: Project,
include: [
{model: User, where: { {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: Project,
include: [
{model: Task, where: {title: 'fight empire'}} {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!