Skip to content

Commit d32f84b

Browse files
WIP: query builder
1 parent c0a38df commit d32f84b

File tree

12 files changed

+1580
-65
lines changed

12 files changed

+1580
-65
lines changed

CHANGELOG.md

Lines changed: 0 additions & 21 deletions
This file was deleted.

packages/core/CHANGELOG.md

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { WhereOptions } from '../abstract-dialect/where-sql-builder-types.js';
2+
import type { FindAttributeOptions, Model, ModelStatic } from '../model.d.ts';
3+
import { BaseSqlExpression, SQL_IDENTIFIER } from './base-sql-expression.js';
4+
5+
/**
6+
* Do not use me directly. Use Model.select() instead.
7+
*/
8+
export class QueryBuilder<M extends Model = Model> extends BaseSqlExpression {
9+
declare protected readonly [SQL_IDENTIFIER]: 'queryBuilder';
10+
11+
private readonly _model: ModelStatic<M>;
12+
private _attributes?: FindAttributeOptions;
13+
private _where?: WhereOptions;
14+
private _isSelect: boolean = false;
15+
16+
constructor(model: ModelStatic<M>) {
17+
super();
18+
this._model = model;
19+
}
20+
21+
/**
22+
* Initialize a SELECT query
23+
*
24+
* @returns The query builder instance for chaining
25+
*/
26+
select(): QueryBuilder<M> {
27+
const newBuilder = new QueryBuilder(this._model);
28+
newBuilder._isSelect = true;
29+
if (this._attributes !== undefined) {
30+
newBuilder._attributes = this._attributes;
31+
}
32+
33+
if (this._where !== undefined) {
34+
newBuilder._where = this._where;
35+
}
36+
37+
return newBuilder;
38+
}
39+
40+
/**
41+
* Specify which attributes to select
42+
*
43+
* @param attributes - Array of attribute names or attribute options
44+
* @returns The query builder instance for chaining
45+
*/
46+
attributes(attributes: FindAttributeOptions): QueryBuilder<M> {
47+
const newBuilder = new QueryBuilder(this._model);
48+
newBuilder._isSelect = this._isSelect;
49+
newBuilder._attributes = attributes;
50+
newBuilder._where = this._where;
51+
52+
return newBuilder;
53+
}
54+
55+
/**
56+
* Add WHERE conditions to the query
57+
*
58+
* @param conditions - Where conditions object
59+
* @returns The query builder instance for chaining
60+
*/
61+
where(conditions: WhereOptions): QueryBuilder<M> {
62+
const newBuilder = new QueryBuilder(this._model);
63+
newBuilder._isSelect = this._isSelect;
64+
if (this._attributes !== undefined) {
65+
newBuilder._attributes = this._attributes;
66+
}
67+
68+
newBuilder._where = conditions;
69+
70+
return newBuilder;
71+
}
72+
73+
/**
74+
* Generate the SQL query string
75+
*
76+
* @returns The SQL query
77+
*/
78+
getQuery(): string {
79+
if (!this._isSelect) {
80+
throw new Error('Query builder requires select() to be called first');
81+
}
82+
83+
const queryGenerator = this._model.queryGenerator;
84+
const tableName = this._model.tableName;
85+
86+
// Build the options object that matches Sequelize's FindOptions pattern
87+
const options: any = {
88+
attributes: this._attributes,
89+
where: this._where,
90+
raw: true,
91+
plain: false,
92+
};
93+
94+
// Generate the SQL using the existing query generator
95+
const sql = queryGenerator.selectQuery(tableName, options, this._model);
96+
97+
return sql;
98+
}
99+
100+
/**
101+
* Get the table name for this query
102+
*
103+
* @returns The table name
104+
*/
105+
get tableName(): string {
106+
return this._model.tableName;
107+
}
108+
109+
/**
110+
* Get the model class
111+
*
112+
* @returns The model class
113+
*/
114+
get model(): ModelStatic<M> {
115+
return this._model;
116+
}
117+
}
118+
119+
/**
120+
* Creates a new QueryBuilder instance for the given model
121+
*
122+
* @param model - The model class
123+
* @returns A new query builder instance
124+
*/
125+
export function createQueryBuilder<M extends Model>(model: ModelStatic<M>): QueryBuilder<M> {
126+
return new QueryBuilder(model);
127+
}

packages/core/src/model.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { Cast } from './expression-builders/cast.js';
2626
import type { Col } from './expression-builders/col.js';
2727
import type { Fn } from './expression-builders/fn.js';
2828
import type { Literal } from './expression-builders/literal.js';
29+
import type { QueryBuilder } from './expression-builders/query-builder.js';
2930
import type { Where } from './expression-builders/where.js';
3031
import type { Lock, Op, TableHints, Transaction, WhereOptions } from './index';
3132
import type { IndexHints } from './index-hints';
@@ -2395,6 +2396,22 @@ export abstract class Model<
23952396
options?: AddScopeOptions,
23962397
): void;
23972398

2399+
/**
2400+
* Creates a new QueryBuilder instance for this model.
2401+
* This enables functional/chainable query building.
2402+
*
2403+
* @returns A new QueryBuilder instance for this model
2404+
*
2405+
* @example
2406+
* ```js
2407+
* const query = User.select()
2408+
* .attributes(['name', 'email'])
2409+
* .where({ active: true })
2410+
* .getQuery();
2411+
* ```
2412+
*/
2413+
static select<M extends Model>(this: ModelStatic<M>): QueryBuilder<M>;
2414+
23982415
/**
23992416
* Search for multiple instances.
24002417
* See {@link https://sequelize.org/docs/v7/core-concepts/model-querying-basics/} for more information about querying.

packages/core/src/model.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { AssociationSecret } from './associations/helpers';
4040
import * as DataTypes from './data-types';
4141
import * as SequelizeErrors from './errors';
4242
import { BaseSqlExpression } from './expression-builders/base-sql-expression.js';
43+
import { QueryBuilder } from './expression-builders/query-builder.js';
4344
import { InstanceValidator } from './instance-validator';
4445
import {
4546
_validateIncludedElements,
@@ -4626,6 +4627,24 @@ Instead of specifying a Model, either:
46264627
static belongsTo(target, options) {
46274628
return BelongsToAssociation.associate(AssociationSecret, this, target, options);
46284629
}
4630+
4631+
/**
4632+
* Creates a new QueryBuilder instance for this model.
4633+
* This enables functional/chainable query building.
4634+
*
4635+
* @returns {QueryBuilder} A new QueryBuilder instance for this model
4636+
*
4637+
* @example
4638+
* ```js
4639+
* const query = User.select()
4640+
* .attributes(['name', 'email'])
4641+
* .where({ active: true })
4642+
* .getQuery();
4643+
* ```
4644+
*/
4645+
static select() {
4646+
return new QueryBuilder(this).select();
4647+
}
46294648
}
46304649

46314650
/**

0 commit comments

Comments
 (0)