Skip to content

feat: query builder #17937

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

Open
wants to merge 24 commits into
base: v6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,11 @@ jobs:
env:
ACCEPT_EULA: Y
SA_PASSWORD: Password12!
MSSQL_SA_PASSWORD: Password12!
ports:
- 1433:1433
options: >-
--health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P \"Password12!\" -l 30 -Q \"SELECT 1\""
--health-cmd="/opt/mssql-tools${{ matrix.mssql-version == '2019' && '18' || '' }}/bin/sqlcmd -S localhost -U SA -P \"Password12!\" -C -l 30 -Q \"SELECT 1\""
--health-start-period 10s
--health-interval 10s
--health-timeout 5s
Expand All @@ -265,7 +266,7 @@ jobs:
SEQ_PW: Password12!
SEQ_PORT: 1433
steps:
- run: /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "Password12!" -Q "CREATE DATABASE sequelize_test; ALTER DATABASE sequelize_test SET READ_COMMITTED_SNAPSHOT ON;"
- run: /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "Password12!" -C -Q "CREATE DATABASE sequelize_test; ALTER DATABASE sequelize_test SET READ_COMMITTED_SNAPSHOT ON;"
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
Expand Down
44 changes: 44 additions & 0 deletions src/dialects/abstract/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,8 @@ https://github.com/sequelize/sequelize/discussions/15694`);
//through
if (include.through) {
joinQuery = this.generateThroughJoin(include, includeAs, parentTableName.internalAs, topLevelInfo);
} else if (include._isCustomJoin) {
joinQuery = this.generateCustomJoin(include, includeAs, topLevelInfo);
} else {
this._generateSubQueryFilter(include, includeAs, topLevelInfo);
joinQuery = this.generateJoin(include, topLevelInfo);
Expand Down Expand Up @@ -1807,6 +1809,48 @@ https://github.com/sequelize/sequelize/discussions/15694`);
return null;
}

generateCustomJoin(include, includeAs, topLevelInfo) {
const right = include.model;
const tableRight = right.getTableName();
const asRight = includeAs.internalAs;
let joinCondition;
let joinWhere;

if (!include.on) throw new Error('Custom joins require an "on" condition to be specified');

// Handle the custom join condition
joinCondition = this.whereItemsQuery(include.on, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});

if (include.where) {
joinWhere = this.whereItemsQuery(include.where, {
prefix: this.sequelize.literal(this.quoteIdentifier(asRight)),
model: include.model
});
if (joinWhere) {
if (include.or) {
joinCondition += ` OR ${joinWhere}`;
} else {
joinCondition += ` AND ${joinWhere}`;
}
}
}

this.aliasAs(asRight, topLevelInfo);

return {
join: include.required ? 'INNER JOIN' : include.right && this._dialect.supports['RIGHT JOIN'] ? 'RIGHT OUTER JOIN' : 'LEFT OUTER JOIN',
body: this.quoteTable(tableRight, asRight),
condition: joinCondition,
attributes: {
main: [],
subQuery: []
}
};
}

generateJoin(include, topLevelInfo) {
const association = include.association;
const parent = include.parent;
Expand Down
17 changes: 17 additions & 0 deletions src/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Sequelize, SyncOptions } from './sequelize';
import { Col, Fn, Literal, Where, MakeNullishOptional, AnyFunction, Cast, Json } from './utils';
import { LOCK, Transaction, Op, Optional } from './index';
import { SetRequired } from './utils/set-required';
import { QueryBuilder } from './query-builder';

// Backport of https://github.com/sequelize/sequelize/blob/a68b439fb3ea748d3f3d37356d9fe610f86184f6/src/utils/index.ts#L85
export type AllowReadonlyArray<T> = T | readonly T[];
Expand Down Expand Up @@ -2104,6 +2105,22 @@ export abstract class Model<TModelAttributes extends {} = any, TCreationAttribut
options?: string | ScopeOptions | readonly (string | ScopeOptions)[] | WhereAttributeHash<M>
): ModelCtor<M>;

/**
* [EXPERIMENTAL] Creates a new QueryBuilder instance for this model.
* This enables functional/chainable query building.
*
* @returns A new QueryBuilder instance for this model
*
* @example
* ```js
* const query = User.select()
* .attributes(['name', 'email'])
* .where({ active: true })
* .getQuery();
* ```
*/
static select<M extends Model>(this: ModelStatic<M>): QueryBuilder<M>;

/**
* Add a new scope to the model
*
Expand Down
5 changes: 5 additions & 0 deletions src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const Hooks = require('./hooks');
const associationsMixin = require('./associations/mixin');
const Op = require('./operators');
const { noDoubleNestedGroup } = require('./utils/deprecations');
const QueryBuilder = require('./query-builder');


// This list will quickly become dated, but failing to maintain this list just means
Expand Down Expand Up @@ -1534,6 +1535,10 @@ class Model {
}
}

static select() {
return new QueryBuilder(this).select();
}

/**
* Apply a scope created in `define` to the model.
*
Expand Down
54 changes: 54 additions & 0 deletions src/query-builder.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
FindAttributeOptions,
GroupOption,
Model,
ModelStatic,
Order,
Sequelize,
WhereOptions,
} from ".";
import { Col, Literal } from "./utils";

type QueryBuilderIncludeOptions<M extends Model> = {
model: ModelStatic<M>;
as?: string;
on?: Record<keyof M, Col>;
attributes?: FindAttributeOptions;
where?: WhereOptions;
required?: boolean;
joinType?: "LEFT" | "INNER" | "RIGHT";
};

type QueryBuilderGetQueryOptions = {
multiline?: boolean;
};

export class QueryBuilder<M extends Model = Model> {
private _attributes: FindAttributeOptions | undefined;
private _where: WhereOptions | undefined;
private _group: GroupOption | undefined;
private _having: Literal[] | undefined;
private _order: Order | undefined;
private _limit: number | undefined;
private _offset: number | undefined;
private _isSelect: boolean;
private _model: M;
private _sequelize: Sequelize;

constructor(model?: M);
clone(): QueryBuilder<M>;
select(): QueryBuilder<M>;
attributes(attributes: FindAttributeOptions): QueryBuilder<M>;
where(conditions: WhereOptions): QueryBuilder<M>;
includes(options: QueryBuilderIncludeOptions<M>): QueryBuilder<M>;
groupBy(group: GroupOption): QueryBuilder<M>;
having(having: Literal): QueryBuilder<M>;
andHaving(having: Literal): QueryBuilder<M>;
orderBy(order: Order | undefined): QueryBuilder<M>;
limit(limit: number): QueryBuilder<M>;
offset(offset: number): QueryBuilder<M>;
getQuery(options?: QueryBuilderGetQueryOptions): string;
execute(): Promise<[unknown[], unknown]>;
get tableName(): string;
get model(): ModelStatic<M>;
}
Loading
Loading