Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: clarify how the limit option works #13985

Merged
merged 13 commits into from Jan 27, 2022
Merged
5 changes: 5 additions & 0 deletions lib/operators.ts
Expand Up @@ -478,6 +478,11 @@ interface OpTypes {
readonly values: unique symbol;
}

// Note: These symbols are registered in the Global Symbol Registry
// to counter bugs when two different versions of this library are loaded
// Source issue: https://github.com/sequelize/sequelize/issues/8663
// This is not an endorsement of having two different versions of the library loaded at the same time,
// a lot more is going to silently break if you do this.
export const Op: OpTypes = {
eq: Symbol.for('eq'),
ne: Symbol.for('ne'),
Expand Down
10 changes: 10 additions & 0 deletions test/support.js
Expand Up @@ -268,6 +268,16 @@ const Support = {
isDeepEqualToOneOf(actual, expectedOptions) {
return expectedOptions.some(expected => isDeepStrictEqual(actual, expected));
},

/**
* Reduces insignificant whitespace from SQL string.
*
* @param {string} sql the SQL string
* @returns {string} the SQL string with insignificant whitespace removed.
*/
minifySql(sql) {
return sql.replace(/\s+/g, ' ').trim();
},
};

if (global.beforeEach) {
Expand Down
32 changes: 30 additions & 2 deletions test/unit/sql/select.test.js
Expand Up @@ -16,10 +16,10 @@ const Op = Support.Sequelize.Op;

describe(Support.getTestDialectTeaser('SQL'), () => {
describe('select', () => {
const testsql = function (options, expectation) {
const testsql = function (options, expectation, testFunction = it) {
const model = options.model;

it(util.inspect(options, { depth: 2 }), () => {
testFunction(util.inspect(options, { depth: 2 }), () => {
return expectsql(
sql.selectQuery(
options.table || model && model.getTableName(),
Expand All @@ -31,6 +31,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
});
};

testsql.only = (options, expectation) => testsql(options, expectation, it.only);

testsql({
table: 'User',
attributes: [
Expand Down Expand Up @@ -300,6 +302,32 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
}) AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id_user] = [POSTS].[user_id] ORDER BY [user].[last_name] ASC;`,
});

// By default, SELECT with include of a multi association & limit will be ran as a subQuery
// This checks the result when the query is forced to be ran without a subquery
testsql({
table: User.getTableName(),
model: User,
include,
attributes: [
['id_user', 'id'],
'email',
['first_name', 'firstName'],
['last_name', 'lastName'],
],
order: [['[last_name]'.replace(/\[/g, Support.sequelize.dialect.TICK_CHAR_LEFT).replace(/\]/g, Support.sequelize.dialect.TICK_CHAR_RIGHT), 'ASC']],
limit: 30,
offset: 10,
hasMultiAssociation: true, // must be set only for mssql dialect here
subQuery: false,
}, {
default: Support.minifySql(`SELECT [user].[id_user] AS [id], [user].[email], [user].[first_name] AS [firstName], [user].[last_name] AS [lastName], [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title]
FROM [users] AS [user] LEFT OUTER JOIN [post] AS [POSTS]
ON [user].[id_user] = [POSTS].[user_id]
ORDER BY [user].[last_name] ASC
${sql.addLimitAndOffset({ limit: 30, offset: 10, order: [['`user`.`last_name`', 'ASC']] })};
`),
});

const nestedInclude = Model._validateIncludedElements({
include: [{
attributes: ['title'],
Expand Down
34 changes: 32 additions & 2 deletions types/lib/model.d.ts
Expand Up @@ -555,10 +555,37 @@ export interface FindOptions<TAttributes = any>
group?: GroupOption;

/**
* Limit the results
* Limits how many items will be retrieved by the operation.
*
* If `limit` and `include` are used together, Sequelize will turn the `subQuery` option on by default.
* This is done to ensure that `limit` only impacts the Model on the same level as the `limit` option.
*
* You can disable this behavior by explicitly setting `subQuery: false`, however `limit` will then
* affect the total count of returned values, including eager-loaded associations, instead of just one table.
*
* @example
* // in the following query, `limit` only affects the "User" model.
* // This will return 2 users, each including all of their projects.
* User.findAll({
* limit: 2,
* include: [User.associations.projects],
* });
*
* @example
* // in the following query, `limit` affects the total number of returned values, eager-loaded associations included.
* // This may return 2 users, each with one project,
* // or 1 user with 2 projects.
* User.findAll({
* limit: 2,
* include: [User.associations.projects],
* subQuery: false,
* });
*/
limit?: number;

// TODO: document this - this is an undocumented property but it exists and there are tests for it.
groupedLimit?: unknown;

/**
* Skip the results;
*/
Expand Down Expand Up @@ -589,7 +616,10 @@ export interface FindOptions<TAttributes = any>
having?: WhereOptions<any>;

/**
* Use sub queries (internal)
* Use sub queries (internal).
*
* If unspecified, this will `true` by default if `limit` is specified, and `false` otherwise.
* See {@link FindOptions#limit} for more information.
*/
subQuery?: boolean;
}
Expand Down