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

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 @@
- [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] 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:
- `hookValidate` removed in favor of `validate` with `hooks: true | false`. `validate` returns a promise which is rejected if validation fails
......@@ -95,6 +96,7 @@
- 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`)
- 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
- [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 = {
potentially also be used for other places where we want to be able to call SQL functions (e.g. as default values)
@private
*/
quote(obj, parent, force) {
quote(obj, parent, force, connector) {
connector = connector || '.';
if (Utils._.isString(obj)) {
return this.quoteIdentifiers(obj, force);
} else if (Array.isArray(obj)) {
......@@ -656,12 +657,12 @@ const QueryGenerator = {
parentAssociation = association;
} else {
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
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 (obj[i + 1]._isSequelizeMethod) {
sql += this.handleSequelizeMethod(obj[i + 1]);
......@@ -748,409 +749,93 @@ const QueryGenerator = {
- offset -> An offset value to start from. Only useable with limit!
@private
*/
selectQuery(tableName, options, model) {
// Enter and change at your own peril -- Mick Hansen
options = options || {};
const limit = options.limit;
const mainModel = model;
const mainQueryItems = [];
const subQuery = options.subQuery === undefined ? limit && options.hasMultiAssociation : options.subQuery;
const subQueryItems = [];
let table = null;
let query;
let mainAttributes = options.attributes && options.attributes.slice();
const subQuery = options.subQuery === undefined ? limit && options.hasMultiAssociation : options.subQuery;
const attributes = {
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 = [];
// We'll use a subquery if we have a hasMany association and a limit
let subQueryAttributes = null;
let subJoinQueries = [];
let mainTableAs = null;
let query;
// resolve table name options
if (options.tableAs) {
mainTableAs = this.quoteTable(options.tableAs);
} else if (!Array.isArray(tableName) && model) {
mainTableAs = this.quoteTable(model.name);
mainTable.as = this.quoteTable(options.tableAs);
} else if (!Array.isArray(mainTable.name) && mainTable.model) {
mainTable.as = this.quoteTable(mainTable.model.name);
}
table = !Array.isArray(tableName) ? this.quoteTable(tableName) : tableName.map(t => {
if (Array.isArray(t)) {
return this.quoteTable(t[0], t[1]);
}
return this.quoteTable(t, true);
mainTable.quotedName = !Array.isArray(mainTable.name) ? this.quoteTable(mainTable.name) : tableName.map(t => {
return Array.isArray(t) ? this.quoteTable(t[0], t[1]) : this.quoteTable(t, true);
}).join(', ');
if (subQuery && mainAttributes) {
for (const keyAtt of model.primaryKeyAttributes) {
if (subQuery && attributes.main) {
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
if (!_.find(mainAttributes, attr => keyAtt === attr || keyAtt === attr[0] || keyAtt === attr[1])) {
mainAttributes.push(model.rawAttributes[keyAtt].field ? [keyAtt, model.rawAttributes[keyAtt].field] : keyAtt);
if (!_.find(attributes.main, attr => keyAtt === attr || keyAtt === attr[0] || keyAtt === attr[1])) {
attributes.main.push(mainTable.model.rawAttributes[keyAtt].field ? [keyAtt, mainTable.model.rawAttributes[keyAtt].field] : keyAtt);
}
}
}
// Escape attributes
mainAttributes = mainAttributes && mainAttributes.map(attr => {
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 + '.*'] : ['*']);
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
subQueryAttributes = mainAttributes;
mainAttributes = [(mainTableAs || table) + '.*'];
attributes.subQuery = attributes.main;
attributes.main = [(mainTable.as || mainTable.quotedName) + '.*'];
}
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) {
if (include.separate) {
continue;
}
const joinQueries = generateJoinQueries(include, mainTableAs);
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 using subQuery select defined subQuery attributes and join subJoinQueries
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(''));
// Else do it the reguar way
} else {
if (options.groupedLimit) {
if (!mainTableAs) {
mainTableAs = table;
if (!mainTable.as) {
mainTable.as = mainTable.quotedName;
}
const where = Object.assign({}, options.where);
let groupedLimitOrder
, whereKey
, include
, groupedTableName = mainTableAs;
, groupedTableName = mainTable.as;
if (typeof options.groupedLimit.on === 'string') {
whereKey = options.groupedLimit.on;
......@@ -1210,7 +895,7 @@ const QueryGenerator = {
// 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(
const baseQuery = '(' + this.selectQuery(
tableName,
{
attributes: options.attributes,
......@@ -1221,11 +906,11 @@ const QueryGenerator = {
model
},
model
).replace(/;$/, '')+')';
).replace(/;$/, '') + ')';
const placeHolder = this.whereItemQuery('$$PLACEHOLDER$$', true, { model });
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 => {
let groupWhere;
if (whereKey) {
......@@ -1241,18 +926,19 @@ const QueryGenerator = {
return Utils.spliceStr(baseQuery, splicePos, placeHolder.length, this.getWhereConditions(groupWhere, groupedTableName));
}).join(
this._dialect.supports['UNION ALL'] ?' UNION ALL ' : ' UNION '
)
+')', mainTableAs));
this._dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION '
)
+ ')', mainTable.as));
} 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(''));
}
// Add WHERE to sub or main query
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 (subQuery) {
subQueryItems.push(' WHERE ' + options.where);
......@@ -1260,8 +946,8 @@ const QueryGenerator = {
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 (value.match(/^SELECT/)) {
mainQueryItems[key] = this.selectFromTableFragment(options, model, attributes.main, mainTable.quotedName, mainTable.as, options.where);
}
});
}
......@@ -1287,10 +973,10 @@ const QueryGenerator = {
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(', '));
}
......@@ -1300,7 +986,7 @@ const QueryGenerator = {
}
// 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 (subQuery) {
subQueryItems.push(limitOrder);
......@@ -1309,13 +995,8 @@ const QueryGenerator = {
}
}
// If using subQuery, select attributes from wrapped subQuery and join out join tables
if (subQuery) {
query = 'SELECT ' + mainAttributes.join(', ') + ' FROM (';
query += subQueryItems.join('');
query += ') AS ' + mainTableAs;
query += mainJoinQueries.join('');
query += mainQueryItems.join('');
query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) AS ${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`;
} else {
query = mainQueryItems.join('');
}
......@@ -1337,128 +1018,268 @@ const QueryGenerator = {
}
}
query += ';';
return query;
return `${query};`;
},
getQueryOrders(options, model, subQuery) {
const mainQueryOrder = [];
const subQueryOrder = [];
escapeAttributes(attributes, options, mainTableAs) {
return attributes && attributes.map(attr => {
let addTable = true;
const validateOrder = order => {
if (order instanceof Utils.Literal) return;
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 (!_.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 (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;
});
},
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)) {
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));
topLevelInfo.options.keysEscaped = true;
if (topLevelInfo.names.name !== parentTableName.externalAs && topLevelInfo.names.as !== parentTableName.externalAs) {
includeAs.internalAs = `${parentTableName.internalAs}->${include.as}`;
includeAs.externalAs = `${parentTableName.externalAs}.${include.as}`;
}
// 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))) {
subQueryOrder.push(this.quote(t, model));
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'
);
}
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 {
var sql = this.quote(typeof options.order === 'string' ? new Utils.Literal(options.order) : options.order, model);
if (subQuery) {
subQueryOrder.push(sql);
if (topLevelInfo.subQuery && include.subQueryFilter) {
const associationWhere = {};
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) {
let fragment = 'SELECT ' + attributes.join(', ') + ' FROM ' + tables;
if (joinQuery.attributes.subQuery.length > 0) {
attributes.subQuery = attributes.subQuery.concat(joinQuery.attributes.subQuery);
}
if(mainTableAs) {
fragment += ' AS ' + mainTableAs;
if (include.include) {
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) {
const subQuery = options.subQuery;
const include = options.include;
generateJoin(include, topLevelInfo) {
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 ';
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;
association.identifier :
left.primaryKeyAttribute;
const fieldLeft = association instanceof BelongsTo ?
association.identifierField :
left.rawAttributes[left.primaryKeyAttribute].field;
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;
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('.');
asLeft = `${$parent.as}->${asLeft}`;
} else {
asLeft = $parent.as;
}
}
if (!asLeft) asLeft = parent.as || parent.model.name;
else asRight = [asLeft, asRight].join('.');
else asRight = `${asLeft}->${asRight}`;
let joinOn = [
this.quoteTable(asLeft),
this.quoteIdentifier(fieldLeft)
].join('.');
let joinOn = `${this.quoteTable(asLeft)}.${this.quoteIdentifier(fieldLeft)}`;
if ((options.groupedLimit && parentIsTop) || (subQuery && include.parent.subQuery && !include.subQuery)) {
if ((topLevelInfo.options.groupedLimit && parentIsTop) || (topLevelInfo.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('.');
joinOn = `${this.quoteTable(parent.as || parent.model.name)}.${this.quoteIdentifier(attrLeft)}`;
} 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) {
joinOn = this.whereItemsQuery(include.on, {
......@@ -1474,14 +1295,244 @@ const QueryGenerator = {
});
if (joinWhere) {
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 {
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 = {
return items.length && items.filter(item => item && item.length).join(binding) || '';
},
whereItemQuery(key, value, options) {
options = options || {};
let binding;
......@@ -2010,7 +2062,8 @@ const QueryGenerator = {
if (value.length > 2) {
value = [
value.slice(0, -1).join('.'),
// join the tables by -> to match out internal namings
value.slice(0, -1).join('->'),
value[value.length - 1]
];
}
......@@ -2069,7 +2122,8 @@ const QueryGenerator = {
if (key.length > 2) {
key = [
key.slice(0, -1).join('.'),
// join the tables by -> to match out internal namings
key.slice(0, -1).join('->'),
key[key.length - 1]
];
}
......
......@@ -750,7 +750,7 @@ const QueryGenerator = {
quoteIdentifier(identifier, force) {
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,
// they are also case sensitive. If they contain any uppercase
// characters, they must always be double-quoted. This makes it
......
......@@ -45,11 +45,16 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() {
}]).then(function() {
return Task.findAll({
include: [
{model: Project, include: [
{model: User, where: {username: 'leia'}}
]}
{
model: Project,
include: [
{model: User, where: {username: 'leia'}}
],
required: true,
}
]
}).then(function(tasks) {
expect(tasks.length).to.be.equal(2);
expect(tasks[0].title).to.be.equal('fight empire');
expect(tasks[1].title).to.be.equal('stablish republic');
......@@ -99,12 +104,16 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() {
}]).then(function() {
return Task.findAll({
include: [
{model: Project, include: [
{model: User, where: {
{
model: Project,
include: [
{model: User, where: {
username: 'leia',
id: 1
}}
]}
}}
],
required: true,
}
]
}).then(function(tasks) {
expect(tasks.length).to.be.equal(2);
......@@ -156,9 +165,13 @@ describe(Support.getTestDialectTeaser('Multiple Level Filters'), function() {
}]).then(function() {
return User.findAll({
include: [
{model: Project, include: [
{model: Task, where: {title: 'fight empire'}}
]}
{
model: Project,
include: [
{model: Task, where: {title: 'fight empire'}}
],
required: true,
}
]
}).then(function(users) {
expect(users.length).to.be.equal(1);
......
......@@ -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() {
]
}
}, {
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] = 5 ORDER BY [user].[last_name] ASC'+sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })+')'
].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() {
}).include,
model: 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];',
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;'
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;'
});
});
......
......@@ -377,7 +377,7 @@ suite(Support.getTestDialectTeaser('SQL'), function() {
testsql('$offer.organization.id$', {
$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!