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

Commit 2f7ac2d8 by Michael Kaufman Committed by Felix Becker

7425 Association identifiers in ordering and groups (#7454)

1 parent 2909ec1e
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- [FIXED] Show a reasonable message when using renameColumn with a missing column [#6606](https://github.com/sequelize/sequelize/issues/6606) - [FIXED] Show a reasonable message when using renameColumn with a missing column [#6606](https://github.com/sequelize/sequelize/issues/6606)
- [PERFORMANCE] more efficient array handing for certain large queries [#7175](https://github.com/sequelize/sequelize/pull/7175) - [PERFORMANCE] more efficient array handing for certain large queries [#7175](https://github.com/sequelize/sequelize/pull/7175)
- [FIXED] Add `unique` indexes defined via options to `rawAttributes` [#7196] - [FIXED] Add `unique` indexes defined via options to `rawAttributes` [#7196]
- [FIXED] Removed support where `order` value is string and interpreted as `Sequelize.literal()`. [#6935](https://github.com/sequelize/sequelize/issues/6935) - [REMOVED] Removed support where `order` value is string and interpreted as `Sequelize.literal()`. [#6935](https://github.com/sequelize/sequelize/issues/6935)
- [CHANGED] `DataTypes.DATE` to use `DATETIMEOFFSET` [MSSQL] [#5403](https://github.com/sequelize/sequelize/issues/5403) - [CHANGED] `DataTypes.DATE` to use `DATETIMEOFFSET` [MSSQL] [#5403](https://github.com/sequelize/sequelize/issues/5403)
- [FIXED] Properly pass options to `sequelize.query` in `removeColumn` [MSSQL] [#7193](https://github.com/sequelize/sequelize/pull/7193) - [FIXED] Properly pass options to `sequelize.query` in `removeColumn` [MSSQL] [#7193](https://github.com/sequelize/sequelize/pull/7193)
- [FIXED] Updating `VIRTUAL` field throw `ER_EMPTY_QUERY` [#6356](https://github.com/sequelize/sequelize/issues/6356) - [FIXED] Updating `VIRTUAL` field throw `ER_EMPTY_QUERY` [#6356](https://github.com/sequelize/sequelize/issues/6356)
...@@ -63,6 +63,8 @@ ...@@ -63,6 +63,8 @@
- [FIXED] `bulkCreate` now runs in O(N) time instead of O(N^2) time. [#4247](https://github.com/sequelize/sequelize/issues/4247) - [FIXED] `bulkCreate` now runs in O(N) time instead of O(N^2) time. [#4247](https://github.com/sequelize/sequelize/issues/4247)
- [FIXED] Passing parameters to model getters [#7404](https://github.com/sequelize/sequelize/issues/7404) - [FIXED] Passing parameters to model getters [#7404](https://github.com/sequelize/sequelize/issues/7404)
- [FIXED] Model.validate runs validation hooks by default [#7182](https://github.com/sequelize/sequelize/pull/7182) - [FIXED] Model.validate runs validation hooks by default [#7182](https://github.com/sequelize/sequelize/pull/7182)
- [ADDED] Added support for associations aliases in orders and groups. [#7425](https://github.com/sequelize/sequelize/issues/7425)
- [REMOVED] Removes support for `{raw: 'injection goes here'}` for order and group. [#7188](https://github.com/sequelize/sequelize/issues/7188)
## BC breaks: ## BC breaks:
- Model.validate instance method now runs validation hooks by default. Previously you needed to pass { hooks: true }. You can override this behavior by passing { hooks: false } - Model.validate instance method now runs validation hooks by default. Previously you needed to pass { hooks: true }. You can override this behavior by passing { hooks: false }
......
...@@ -305,8 +305,6 @@ something.findOne({ ...@@ -305,8 +305,6 @@ something.findOne({
// will return otherfunction(`col1`, 12, 'lalala') DESC // will return otherfunction(`col1`, 12, 'lalala') DESC
[sequelize.fn('otherfunction', sequelize.fn('awesomefunction', sequelize.col('col'))), 'DESC'] [sequelize.fn('otherfunction', sequelize.fn('awesomefunction', sequelize.col('col'))), 'DESC']
// will return otherfunction(awesomefunction(`col`)) DESC, This nesting is potentially infinite! // will return otherfunction(awesomefunction(`col`)) DESC, This nesting is potentially infinite!
[{ raw: 'otherfunction(awesomefunction(`col`))' }, 'DESC']
// This won't be quoted, but direction will be added
] ]
}) })
``` ```
......
...@@ -270,10 +270,10 @@ Project.findAll({ offset: 5, limit: 5 }) ...@@ -270,10 +270,10 @@ Project.findAll({ offset: 5, limit: 5 })
`order` takes an array of items to order the query by or a sequelize method. Generally you will want to use a tuple/array of either attribute, direction or just direction to ensure proper escaping. `order` takes an array of items to order the query by or a sequelize method. Generally you will want to use a tuple/array of either attribute, direction or just direction to ensure proper escaping.
```js ```js
something.findOne({ Subtask.findAll({
order: [ order: [
// Will escape username and validate DESC against a list of valid direction parameters // Will escape username and validate DESC against a list of valid direction parameters
['username', 'DESC'], ['title', 'DESC'],
// Will order by max(age) // Will order by max(age)
sequelize.fn('max', sequelize.col('age')), sequelize.fn('max', sequelize.col('age')),
...@@ -284,22 +284,38 @@ something.findOne({ ...@@ -284,22 +284,38 @@ something.findOne({
// Will order by otherfunction(`col1`, 12, 'lalala') DESC // Will order by otherfunction(`col1`, 12, 'lalala') DESC
[sequelize.fn('otherfunction', sequelize.col('col1'), 12, 'lalala'), 'DESC'], [sequelize.fn('otherfunction', sequelize.col('col1'), 12, 'lalala'), 'DESC'],
// Will order by name on an associated User // Will order an associated model's created_at using the model name as the association's name.
[User, 'name', 'DESC'], [Task, 'createdAt', 'DESC'],
// Will order by name on an associated User aliased as Friend // Will order through an associated model's created_at using the model names as the associations' names.
[{model: User, as: 'Friend'}, 'name', 'DESC'], [Task, Project, 'createdAt', 'DESC'],
// Will order by name on a nested associated Company of an associated User // Will order by an associated model's created_at using the name of the association.
[User, Company, 'name', 'DESC'], ['Task', 'createdAt', 'DESC'],
// Will order by a nested associated model's created_at using the names of the associations.
['Task', 'Project', 'createdAt', 'DESC'],
// Will order by an associated model's created_at using an association object. (preferred method)
[Subtask.associations.Task, 'createdAt', 'DESC'],
// Will order by a nested associated model's created_at using association objects. (preferred method)
[Subtask.associations.Task, Task.associations.Project, 'createdAt', 'DESC'],
// Will order by an associated model's created_at using a simple association object.
[{model: Task, as: 'Task'}, 'createdAt', 'DESC'],
// Will order by a nested associated model's created_at simple association objects.
[{model: Task, as: 'Task'}, {model: Project, as: 'Project'}, 'createdAt', 'DESC']
] ]
// Will order by max age descending // Will order by max age descending
order: sequelize.literal('max(age) DESC') order: sequelize.literal('max(age) DESC')
// Will order by max age ascencding assuming ascencding is the default order when direction is omitted // Will order by max age ascending assuming ascending is the default order when direction is omitted
order: sequelize.fn('max', sequelize.col('age')) order: sequelize.fn('max', sequelize.col('age'))
// Will order by age ascencding assuming ascencding is the default order when direction is omitted // Will order by age ascending assuming ascending is the default order when direction is omitted
order: sequelize.col('age') order: sequelize.col('age')
}) })
``` ```
...@@ -7,6 +7,7 @@ const DataTypes = require('../../data-types'); ...@@ -7,6 +7,7 @@ const DataTypes = require('../../data-types');
const util = require('util'); const util = require('util');
const _ = require('lodash'); const _ = require('lodash');
const Dottie = require('dottie'); const Dottie = require('dottie');
const Association = require('../../associations/base');
const BelongsTo = require('../../associations/belongs-to'); const BelongsTo = require('../../associations/belongs-to');
const BelongsToMany = require('../../associations/belongs-to-many'); const BelongsToMany = require('../../associations/belongs-to-many');
const HasMany = require('../../associations/has-many'); const HasMany = require('../../associations/has-many');
...@@ -600,7 +601,8 @@ const QueryGenerator = { ...@@ -600,7 +601,8 @@ const QueryGenerator = {
Strings: should proxy to quoteIdentifiers Strings: should proxy to quoteIdentifiers
Arrays: Arrays:
* Expects array in the form: [<model> (optional), <model> (optional),... String, String (optional)] * Expects array in the form: [<model> (optional), <model> (optional),... String, String (optional)]
Each <model> can be a model or an object {model: Model, as: String}, matching include Each <model> can be a model, or an object {model: Model, as: String}, matching include, or an
association object, or the name of an association.
* Zero or more models can be included in the array and are used to trace a path through the tree of * Zero or more models can be included in the array and are used to trace a path through the tree of
included nested associations. This produces the correct table name for the ORDER BY/GROUP BY SQL included nested associations. This produces the correct table name for the ORDER BY/GROUP BY SQL
and quotes it. and quotes it.
...@@ -617,72 +619,139 @@ const QueryGenerator = { ...@@ -617,72 +619,139 @@ 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, connector) { quote(collection, parent, connector) {
// init
const validOrderOptions = [
'ASC',
'DESC',
'ASC NULLS LAST',
'DESC NULLS LAST',
'ASC NULLS FIRST',
'DESC NULLS FIRST',
'NULLS FIRST',
'NULLS LAST'
];
// default
connector = connector || '.'; connector = connector || '.';
if (_.isString(obj)) {
return this.quoteIdentifiers(obj, force); // just quote as identifiers if string
} else if (Array.isArray(obj)) { if (typeof collection === 'string'){
return this.quoteIdentifiers(collection);
} else if (Array.isArray(collection)) {
// iterate through the collection and mutate objects into associations
collection.forEach((item, index) => {
const previous = collection[index - 1];
let previousAssociation;
let previousModel;
// set the previous as the parent when previous is undefined or the target of the association
if(!previous && parent !== undefined){
previousModel = parent;
} else if (previous && previous instanceof Association) {
previousAssociation = previous;
previousModel = previous.target;
}
// if the previous item is a model, then attempt getting an association
if (previousModel && previousModel.prototype instanceof Model) {
let model;
let as;
if (typeof item === 'function' && item.prototype instanceof Model) {
// set
model = item;
} else if (_.isPlainObject(item) && item.model && item.model.prototype instanceof Model) {
// set
model = item.model;
as = item.as;
}
if (model) {
// set the as to either the through name or the model name
if (!as && previousAssociation && previousAssociation instanceof Association && previousAssociation.through && previousAssociation.through.model === model) {
// get from previous association
item = new Association(previousModel, model, {
as: model.name
});
} else {
// get association from previous model
item = previousModel.getAssociationForAlias(model, as);
// attempt to use the model name if the item is still null
if (!item) {
item = previousModel.getAssociationForAlias(model, model.name);
}
}
// make sure we have an association
if (!(item instanceof Association)) {
throw new Error(util.format('Unable to find a valid association for model, \'%s\'', model.name));
}
}
}
if (typeof item === 'string') {
// get order index
const orderIndex = validOrderOptions.indexOf(item.toUpperCase());
// see if this is an order
if (index > 0 && orderIndex !== -1) {
item = this.sequelize.literal(' ' + validOrderOptions[orderIndex]);
} else if (previousModel && previousModel.prototype instanceof Model) {
// only go down this path if we have preivous model and check only once
if (previousModel.associations !== undefined && previousModel.associations[item]) {
// convert the item to an association
item = previousModel.associations[item];
} else if (previousModel.rawAttributes !== undefined && previousModel.rawAttributes[item] && item !== previousModel.rawAttributes[item].field) {
// convert the item attribute from it's alias
item = previousModel.rawAttributes[item].field;
}
}
}
collection[index] = item;
}, this);
// loop through array, adding table names of models to quoted // loop through array, adding table names of models to quoted
// (checking associations to see if names should be singularised or not) const collectionLength = collection.length;
const len = obj.length;
const tableNames = []; const tableNames = [];
let parentAssociation;
let item; let item;
let model;
let as;
let association;
let i = 0; let i = 0;
for (i = 0; i < len - 1; i++) { for (i = 0; i < collectionLength - 1; i++) {
item = obj[i]; item = collection[i];
if (item._modelAttribute || _.isString(item) || item instanceof Utils.SequelizeMethod || 'raw' in item) { if (typeof item === 'string' || item._modelAttribute || item instanceof Utils.SequelizeMethod) {
break; break;
} else if (item instanceof Association) {
tableNames[i] = item.as;
} }
}
if (typeof item === 'function' && item.prototype instanceof Model) { // start building sql
model = item; let sql = '';
as = undefined;
} else {
model = item.model;
as = item.as;
}
// check if model provided is through table
if (!as && parentAssociation && parentAssociation.through && parentAssociation.through.model === model) {
association = {as: model.name};
} else {
// find applicable association for linking parent to this model
association = parent.getAssociationForAlias(model, as);
}
if (association) { if (i > 0) {
tableNames[i] = association.as; sql += this.quoteIdentifier(tableNames.join(connector)) + '.';
parent = model; } else if (typeof collection[0] === 'string' && parent) {
parentAssociation = association; sql += this.quoteIdentifier(parent.name) + '.';
} else {
tableNames[i] = model.tableName;
throw new Error('\'' + tableNames.join(connector) + '\' in order / group clause is not valid association');
}
} }
// add 1st string as quoted, 2nd as unquoted raw // loop through everything past i and append to the sql
let sql = (i > 0 ? this.quoteIdentifier(tableNames.join(connector)) + '.' : (_.isString(obj[0]) && parent ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force); collection.slice(i).forEach((collectionItem) => {
if (i < len - 1) { sql += this.quote(collectionItem, parent, connector);
if (obj[i + 1] instanceof Utils.SequelizeMethod) { }, this);
sql += this.handleSequelizeMethod(obj[i + 1]);
} else {
sql += ' ' + obj[i + 1];
}
}
return sql; return sql;
} else if (obj._modelAttribute) { } else if (collection._modelAttribute) {
return this.quoteTable(obj.Model.name) + '.' + obj.fieldName; return this.quoteTable(collection.Model.name) + '.' + this.quoteIdentifier(collection.fieldName);
} else if (obj instanceof Utils.SequelizeMethod) { } else if (collection instanceof Utils.SequelizeMethod) {
return this.handleSequelizeMethod(obj); return this.handleSequelizeMethod(collection);
} else if (_.isObject(obj) && 'raw' in obj) { } else if (_.isPlainObject(collection) && collection.raw) {
return obj.raw; // simple objects with raw is no longer supported
throw new Error('The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.');
} else { } else {
throw new Error('Unknown structure passed to order / group: ' + JSON.stringify(obj)); throw new Error('Unknown structure passed to order / group: ' + util.inspect(collection));
} }
}, },
...@@ -1483,47 +1552,29 @@ const QueryGenerator = { ...@@ -1483,47 +1552,29 @@ const QueryGenerator = {
getQueryOrders(options, model, subQuery) { getQueryOrders(options, model, subQuery) {
const mainQueryOrder = []; const mainQueryOrder = [];
const subQueryOrder = []; const subQueryOrder = [];
const validOrderOptions = [
'ASC',
'DESC',
'ASC NULLS LAST',
'DESC NULLS LAST',
'ASC NULLS FIRST',
'DESC NULLS FIRST',
'NULLS FIRST',
'NULLS LAST'
];
const validateOrder = order => {
if (order instanceof Utils.SequelizeMethod) {
return;
}
if (!_.includes(validOrderOptions, order.toUpperCase())) {
throw new Error(util.format('Order must be \'ASC\' or \'DESC\', \'%s\' given', order));
}
};
if (Array.isArray(options.order)) { if (Array.isArray(options.order)) {
for (const order of options.order) { for (let order of options.order) {
if (Array.isArray(order) && _.size(order) > 1) { // wrap if not array
if (typeof order[0] === 'function' && order[0].prototype instanceof Model || typeof order[0].model === 'function' && order[0].model.prototype instanceof Model) { if (!Array.isArray(order)) {
if (_.isString(order[order.length - 2])) { order = [order];
validateOrder(_.last(order));
}
} else {
validateOrder(_.last(order));
}
} }
if (subQuery && (Array.isArray(order) && !(typeof order[0] === 'function' && order[0].prototype instanceof Model) && !(order[0] && typeof order[0].model === 'function' && order[0].model.prototype instanceof Model))) { if (
subQueryOrder.push(this.quote(order, model, false, '->')); subQuery
&& Array.isArray(order)
&& order[0]
&& !(order[0] instanceof Association)
&& !(typeof order[0] === 'function' && order[0].prototype instanceof Model)
&& !(typeof order[0].model === 'function' && order[0].model.prototype instanceof Model)
&& !(typeof order[0] === 'string' && model && model.associations !== undefined && model.associations[order[0]])
) {
subQueryOrder.push(this.quote(order, model, '->'));
} }
mainQueryOrder.push(this.quote(order, model, '->'));
mainQueryOrder.push(this.quote(order, model, false, '->'));
} }
} else if (options.order instanceof Utils.SequelizeMethod){ } else if (options.order instanceof Utils.SequelizeMethod){
const sql = this.quote(options.order, model, false, '->'); const sql = this.quote(options.order, model, '->');
if (subQuery) { if (subQuery) {
subQueryOrder.push(sql); subQueryOrder.push(sql);
} }
......
...@@ -183,39 +183,6 @@ function mapOptionFieldNames(options, Model) { ...@@ -183,39 +183,6 @@ function mapOptionFieldNames(options, Model) {
options.where = mapWhereFieldNames(options.where, Model); options.where = mapWhereFieldNames(options.where, Model);
} }
if (Array.isArray(options.order)) {
for (const oGroup of options.order) {
let OrderModel;
let attr;
let attrOffset;
if (Array.isArray(oGroup)) {
OrderModel = Model;
// Check if we have ['attr', 'DESC'] or [Model, 'attr', 'DESC']
if (typeof oGroup[oGroup.length - 2] === 'string') {
attrOffset = 2;
// Assume ['attr'], [Model, 'attr'] or [seq.fn('somefn', 1), 'DESC']
} else {
attrOffset = 1;
}
attr = oGroup[oGroup.length - attrOffset];
if (oGroup.length > attrOffset) {
OrderModel = oGroup[oGroup.length - (attrOffset + 1)];
if (OrderModel.model) {
OrderModel = OrderModel.model;
}
}
if (OrderModel.rawAttributes && OrderModel.rawAttributes[attr] && attr !== OrderModel.rawAttributes[attr].field) {
oGroup[oGroup.length - attrOffset] = OrderModel.rawAttributes[attr].field;
}
}
}
}
return options; return options;
} }
exports.mapOptionFieldNames = mapOptionFieldNames; exports.mapOptionFieldNames = mapOptionFieldNames;
......
...@@ -73,23 +73,6 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -73,23 +73,6 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return this.sequelize.sync({force: true}); return this.sequelize.sync({force: true});
}); });
it('should throw when 2nd order argument is not ASC or DESC', function () {
return expect(this.User.findAll({
order: [
['id', ';DELETE YOLO INJECTIONS']
]
})).to.eventually.be.rejectedWith(Error, 'Order must be \'ASC\' or \'DESC\', \';DELETE YOLO INJECTIONS\' given');
});
it('should throw with include when last order argument is not ASC or DESC', function () {
return expect(this.User.findAll({
include: [this.Group],
order: [
[this.Group, 'id', ';DELETE YOLO INJECTIONS']
]
})).to.eventually.be.rejectedWith(Error, 'Order must be \'ASC\' or \'DESC\', \';DELETE YOLO INJECTIONS\' given');
});
if (current.dialect.supports['ORDER NULLS']) { if (current.dialect.supports['ORDER NULLS']) {
it('should not throw with on NULLS LAST/NULLS FIRST', function () { it('should not throw with on NULLS LAST/NULLS FIRST', function () {
return this.User.findAll({ return this.User.findAll({
......
...@@ -212,11 +212,6 @@ if (dialect === 'mysql') { ...@@ -212,11 +212,6 @@ if (dialect === 'mysql') {
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
}, { }, {
title: 'raw arguments are neither quoted nor escaped',
arguments: ['myTable', {order: [[{raw: 'f1(f2(id))'}, 'DESC']]}],
expectation: 'SELECT * FROM `myTable` ORDER BY f1(f2(id)) DESC;',
context: QueryGenerator
}, {
title: 'functions can take functions as arguments', title: 'functions can take functions as arguments',
arguments: ['myTable', function(sequelize) { arguments: ['myTable', function(sequelize) {
return { return {
...@@ -577,6 +572,7 @@ if (dialect === 'mysql') { ...@@ -577,6 +572,7 @@ if (dialect === 'mysql') {
} }
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' }); QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect; QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments); var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation); expect(conditions).to.deep.equal(test.expectation);
}); });
......
...@@ -279,25 +279,20 @@ if (dialect.match(/^postgres/)) { ...@@ -279,25 +279,20 @@ if (dialect.match(/^postgres/)) {
expectation: 'SELECT * FROM "myTable" AS "myTable" ORDER BY "myTable"."id" DESC;', expectation: 'SELECT * FROM "myTable" AS "myTable" ORDER BY "myTable"."id" DESC;',
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
},{ }, {
arguments: ['myTable', {order: [['id', 'DESC'], ['name']]}, function(sequelize) {return sequelize.define('myTable', {});}], arguments: ['myTable', {order: [['id', 'DESC'], ['name']]}, function(sequelize) {return sequelize.define('myTable', {});}],
expectation: 'SELECT * FROM "myTable" AS "myTable" ORDER BY "myTable"."id" DESC, "myTable"."name";', expectation: 'SELECT * FROM "myTable" AS "myTable" ORDER BY "myTable"."id" DESC, "myTable"."name";',
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
},{ }, {
title: 'uses limit 0', title: 'uses limit 0',
arguments: ['myTable', {limit: 0}], arguments: ['myTable', {limit: 0}],
expectation: 'SELECT * FROM "myTable" LIMIT 0;', expectation: 'SELECT * FROM "myTable" LIMIT 0;',
context: QueryGenerator context: QueryGenerator
}, { }, {
title: 'uses offset 0', title: 'uses offset 0',
arguments: ['myTable', {offset: 0}], arguments: ['myTable', {offset: 0}],
expectation: 'SELECT * FROM "myTable" OFFSET 0;', expectation: 'SELECT * FROM "myTable" OFFSET 0;',
context: QueryGenerator
}, {
title: 'raw arguments are neither quoted nor escaped',
arguments: ['myTable', {order: [[{raw: 'f1(f2(id))'},'DESC']]}],
expectation: 'SELECT * FROM "myTable" ORDER BY f1(f2(id)) DESC;',
context: QueryGenerator context: QueryGenerator
}, { }, {
title: 'sequelize.where with .fn as attribute and default comparator', title: 'sequelize.where with .fn as attribute and default comparator',
...@@ -325,7 +320,7 @@ if (dialect.match(/^postgres/)) { ...@@ -325,7 +320,7 @@ if (dialect.match(/^postgres/)) {
expectation: 'SELECT * FROM "myTable" WHERE (LOWER("user"."name") LIKE \'%t%\' AND "myTable"."type" = 1);', expectation: 'SELECT * FROM "myTable" WHERE (LOWER("user"."name") LIKE \'%t%\' AND "myTable"."type" = 1);',
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
},{ }, {
title: 'functions can take functions as arguments', title: 'functions can take functions as arguments',
arguments: ['myTable', function(sequelize) { arguments: ['myTable', function(sequelize) {
return { return {
...@@ -923,6 +918,7 @@ if (dialect.match(/^postgres/)) { ...@@ -923,6 +918,7 @@ if (dialect.match(/^postgres/)) {
} }
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' }); QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect; QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments); var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation); expect(conditions).to.deep.equal(test.expectation);
}); });
......
...@@ -198,11 +198,6 @@ if (dialect === 'sqlite') { ...@@ -198,11 +198,6 @@ if (dialect === 'sqlite') {
context: QueryGenerator, context: QueryGenerator,
needsSequelize: true needsSequelize: true
}, { }, {
title: 'raw arguments are neither quoted nor escaped',
arguments: ['myTable', {order: [[{raw: 'f1(f2(id))'}, 'DESC']]}],
expectation: 'SELECT * FROM `myTable` ORDER BY f1(f2(id)) DESC;',
context: QueryGenerator
}, {
title: 'sequelize.where with .fn as attribute and default comparator', title: 'sequelize.where with .fn as attribute and default comparator',
arguments: ['myTable', function(sequelize) { arguments: ['myTable', function(sequelize) {
return { return {
...@@ -536,6 +531,7 @@ if (dialect === 'sqlite') { ...@@ -536,6 +531,7 @@ if (dialect === 'sqlite') {
} }
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' }); QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect; QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments); var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation); expect(conditions).to.deep.equal(test.expectation);
}); });
......
'use strict';
/* jshint -W110 */
const util = require('util');
const chai = require('chai');
const expect = chai.expect;
const Support = require(__dirname + '/../support');
const DataTypes = require(__dirname + '/../../../lib/data-types');
const Model = require(__dirname + '/../../../lib/model');
const expectsql = Support.expectsql;
const current = Support.sequelize;
const 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
describe(Support.getTestDialectTeaser('SQL'), () => {
describe('order', () => {
const testsql = (options, expectation) => {
const model = options.model;
it(util.inspect(options, {depth: 2}), () => {
return expectsql(
sql.selectQuery(
options.table || model && model.getTableName(),
options,
options.model
),
expectation
);
});
};
// models
const User = Support.sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'user',
timestamps: true
});
const Project = Support.sequelize.define('Project', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'project',
timestamps: true
});
const ProjectUser = Support.sequelize.define('ProjectUser', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
userId: {
type: DataTypes.INTEGER,
field: 'user_id',
allowNull: false
},
projectId: {
type: DataTypes.INTEGER,
field: 'project_id',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'project_user',
timestamps: true
});
const Task = Support.sequelize.define('Task', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
projectId: {
type: DataTypes.INTEGER,
field: 'project_id',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'task',
timestamps: true
});
const Subtask = Support.sequelize.define('Subtask', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
taskId: {
type: DataTypes.INTEGER,
field: 'task_id',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'subtask',
timestamps: true
});
// Relations
User.belongsToMany(Project, {
as: 'ProjectUserProjects',
through: ProjectUser,
foreignKey: 'user_id',
otherKey: 'project_id'
});
Project.belongsToMany(User, {
as: 'ProjectUserUsers',
through: ProjectUser,
foreignKey: 'project_id',
otherKey: 'user_id'
});
Project.hasMany(Task, {
as: 'Tasks',
foreignKey: 'project_id'
});
ProjectUser.belongsTo(User, {
as: 'User',
foreignKey: 'user_id'
});
ProjectUser.belongsTo(User, {
as: 'Project',
foreignKey: 'project_id'
});
Task.belongsTo(Project, {
as: 'Project',
foreignKey: 'project_id'
});
Task.hasMany(Subtask, {
as: 'Subtasks',
foreignKey: 'task_id'
});
Subtask.belongsTo(Task, {
as: 'Task',
foreignKey: 'task_id'
});
testsql({
model: Subtask,
attributes: [
'id',
'name',
'createdAt'
],
include: Model._validateIncludedElements({
include: [
{
association: Subtask.associations.Task,
required: true,
attributes: [
'id',
'name',
'createdAt'
],
include: [
{
association: Task.associations.Project,
required: true,
attributes: [
'id',
'name',
'createdAt'
]
}
]
}
],
model: Subtask
}).include,
order: [
// order with multiple simple association syntax with direction
[
{
model: Task,
as: 'Task'
},
{
model: Project,
as: 'Project'
},
'createdAt',
'ASC'
],
// order with multiple simple association syntax without direction
[
{
model: Task,
as: 'Task'
},
{
model: Project,
as: 'Project'
},
'createdAt'
],
// order with simple association syntax with direction
[
{
model: Task,
as: 'Task'
},
'createdAt',
'ASC'
],
// order with simple association syntax without direction
[
{
model: Task,
as: 'Task'
},
'createdAt'
],
// through model object as array with direction
[Task, Project, 'createdAt', 'ASC'],
// through model object as array without direction
[Task, Project, 'createdAt'],
// model object as array with direction
[Task, 'createdAt', 'ASC'],
// model object as array without direction
[Task, 'createdAt'],
// through association object as array with direction
[Subtask.associations.Task, Task.associations.Project, 'createdAt', 'ASC'],
// through association object as array without direction
[Subtask.associations.Task, Task.associations.Project, 'createdAt'],
// association object as array with direction
[Subtask.associations.Task, 'createdAt', 'ASC'],
// association object as array without direction
[Subtask.associations.Task, 'createdAt'],
// through association name order as array with direction
['Task', 'Project', 'createdAt', 'ASC'],
// through association name as array without direction
['Task', 'Project', 'createdAt'],
// association name as array with direction
['Task', 'createdAt', 'ASC'],
// association name as array without direction
['Task', 'createdAt'],
// main order as array with direction
['createdAt', 'ASC'],
// main order as array without direction
['createdAt'],
// main order as string
'createdAt'
]
}, {
default: 'SELECT [Subtask].[id], [Subtask].[name], [Subtask].[createdAt], [Task].[id] AS [Task.id], [Task].[name] AS [Task.name], [Task].[created_at] AS [Task.createdAt], [Task->Project].[id] AS [Task.Project.id], [Task->Project].[name] AS [Task.Project.name], [Task->Project].[created_at] AS [Task.Project.createdAt] FROM [subtask] AS [Subtask] INNER JOIN [task] AS [Task] ON [Subtask].[task_id] = [Task].[id] INNER JOIN [project] AS [Task->Project] ON [Task].[project_id] = [Task->Project].[id] ORDER BY [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Subtask].[created_at] ASC, [Subtask].[created_at], [Subtask].[created_at];',
postgres: 'SELECT "Subtask"."id", "Subtask"."name", "Subtask"."createdAt", "Task"."id" AS "Task.id", "Task"."name" AS "Task.name", "Task"."created_at" AS "Task.createdAt", "Task->Project"."id" AS "Task.Project.id", "Task->Project"."name" AS "Task.Project.name", "Task->Project"."created_at" AS "Task.Project.createdAt" FROM "subtask" AS "Subtask" INNER JOIN "task" AS "Task" ON "Subtask"."task_id" = "Task"."id" INNER JOIN "project" AS "Task->Project" ON "Task"."project_id" = "Task->Project"."id" ORDER BY "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Subtask"."created_at" ASC, "Subtask"."created_at", "Subtask"."created_at";'
});
describe('Invalid', () => {
it('Error on invalid association', () => {
return expect(Subtask.findAll({
order: [
[Project, 'createdAt', 'ASC']
]
})).to.eventually.be.rejectedWith(Error, 'Unable to find a valid association for model, \'Project\'');
});
it('Error on invalid structure', () => {
return expect(Subtask.findAll({
order: [
[Subtask.associations.Task, 'createdAt', Task.associations.Project, 'ASC']
]
})).to.eventually.be.rejectedWith(Error, 'Unknown structure passed to order / group: Project');
});
it('Error when the order is a string', () => {
return expect(Subtask.findAll({
order: 'i am a silly string'
})).to.eventually.be.rejectedWith(Error, 'Order must be type of array or instance of a valid sequelize method.');
});
it('Error when the order contains a `{raw: "..."}` object', () => {
return expect(Subtask.findAll({
order: [
{
raw: 'this should throw an error'
}
]
})).to.eventually.be.rejectedWith(Error, 'The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.');
});
it('Error when the order contains a `{raw: "..."}` object wrapped in an array', () => {
return expect(Subtask.findAll({
order: [
[
{
raw: 'this should throw an error'
}
]
]
})).to.eventually.be.rejectedWith(Error, 'The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.');
});
});
});
});
...@@ -195,255 +195,6 @@ suite(Support.getTestDialectTeaser('Utils'), () => { ...@@ -195,255 +195,6 @@ suite(Support.getTestDialectTeaser('Utils'), () => {
} }
}); });
}); });
test('string field order', function() {
expect(Utils.mapOptionFieldNames({
order: 'firstName DESC'
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: 'firstName DESC'
});
});
test('string in array order', function() {
expect(Utils.mapOptionFieldNames({
order: ['firstName DESC']
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: ['firstName DESC']
});
});
test('single field alias order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: [['first_name', 'DESC']]
});
});
test('multi field alias order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
},
lastName: {
type: DataTypes.STRING,
field: 'last_name'
}
}))).to.eql({
order: [['first_name', 'DESC'], ['last_name', 'ASC']]
});
});
test('multi field alias no direction order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName'], ['lastName']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
},
lastName: {
type: DataTypes.STRING,
field: 'last_name'
}
}))).to.eql({
order: [['first_name'], ['last_name']]
});
});
test('field alias to another field order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'lastName'
},
lastName: {
type: DataTypes.STRING,
field: 'firstName'
}
}))).to.eql({
order: [['lastName', 'DESC']]
});
});
test('multi field no alias order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
},
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [['firstName', 'DESC'], ['lastName', 'ASC']]
});
});
test('multi field alias sub model order', function() {
const Location = Support.sequelize.define('Location', {
latLong: {
type: DataTypes.STRING,
field: 'lat_long'
}
});
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[Item, Location, 'latLong', 'DESC'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[Item, Location, 'lat_long', 'DESC'], ['lastName', 'ASC']]
});
});
test('multi field alias sub model no direction order', function() {
const Location = Support.sequelize.define('Location', {
latLong: {
type: DataTypes.STRING,
field: 'lat_long'
}
});
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[Item, Location, 'latLong'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[Item, Location, 'lat_long'], ['lastName', 'ASC']]
});
});
test('function order', function() {
const fn = Support.sequelize.fn('otherfn', 123);
expect(Utils.mapOptionFieldNames({
order: [[fn, 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[fn, 'ASC']]
});
});
test('function no direction order', function() {
const fn = Support.sequelize.fn('otherfn', 123);
expect(Utils.mapOptionFieldNames({
order: [[fn]]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[fn]]
});
});
test('string no direction order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: [['first_name']]
});
});
test('model alias order', function() {
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[{ model: Item, as: 'another'}, 'fontColor', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
},
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[{ model: Item, as: 'another'}, 'font_color', 'ASC']]
});
});
test('model alias no direction order', function() {
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[{ model: Item, as: 'another'}, 'fontColor']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[{ model: Item, as: 'another'}, 'font_color']]
});
});
test('model alias wrong field order', function() {
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[{ model: Item, as: 'another'}, 'firstName', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[{ model: Item, as: 'another'}, 'firstName', 'ASC']]
});
});
}); });
suite('stack', () => { suite('stack', () => {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!