metal-orm 1.0.97 → 1.0.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1133 +1,1155 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { ColumnDef } from '../schema/column-types.js';
3
- import { OrderingTerm, SelectQueryNode, SetOperationKind } from '../core/ast/query.js';
4
- import { HydrationPlan } from '../core/hydration/types.js';
5
- import {
6
- ColumnNode,
7
- ExpressionNode,
8
- FunctionNode,
9
- BinaryExpressionNode,
10
- CaseExpressionNode,
11
- WindowFunctionNode,
12
- and,
13
- exists,
14
- notExists,
15
- OperandNode
16
- } from '../core/ast/expression.js';
17
- import type { TypedExpression } from '../core/ast/expression.js';
18
- import { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
19
- import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
20
-
21
- type SelectDialectInput = Dialect | DialectKey;
22
-
23
- import { SelectQueryState } from './select-query-state.js';
24
- import { HydrationManager } from './hydration-manager.js';
25
- import {
26
- resolveSelectQueryBuilderDependencies,
27
- SelectQueryBuilderContext,
28
- SelectQueryBuilderDependencies,
29
- SelectQueryBuilderEnvironment
30
- } from './select-query-builder-deps.js';
31
- import { ColumnSelector } from './column-selector.js';
32
- import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
33
- import { RelationKinds } from '../schema/relation.js';
34
- import {
35
- RelationIncludeInput,
36
- RelationIncludeNodeInput,
37
- NormalizedRelationIncludeTree,
38
- cloneRelationIncludeTree,
39
- mergeRelationIncludeTrees,
40
- normalizeRelationInclude,
41
- normalizeRelationIncludeNode
42
- } from './relation-include-tree.js';
43
- import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
44
- import { EntityInstance, RelationMap } from '../schema/types.js';
45
- import type { ColumnToTs, InferRow } from '../schema/types.js';
46
- import { OrmSession } from '../orm/orm-session.ts';
47
- import { ExecutionContext } from '../orm/execution-context.js';
48
- import { HydrationContext } from '../orm/hydration-context.js';
49
- import { executeHydrated, executeHydratedPlain, executeHydratedWithContexts } from '../orm/execute.js';
50
- import { EntityConstructor } from '../orm/entity-metadata.js';
51
- import { materializeAs } from '../orm/entity-materializer.js';
52
- import { resolveSelectQuery } from './query-resolution.js';
53
- import {
54
- applyOrderBy,
55
- buildWhereHasPredicate,
56
- executeCount,
57
- executePagedQuery,
58
- PaginatedResult,
59
- RelationCallback,
60
- WhereHasOptions
61
- } from './select/select-operations.js';
62
- export type { PaginatedResult };
63
- import { SelectFromFacet } from './select/from-facet.js';
64
- import { SelectJoinFacet } from './select/join-facet.js';
65
- import { SelectProjectionFacet } from './select/projection-facet.js';
66
- import { SelectPredicateFacet } from './select/predicate-facet.js';
67
- import { SelectCTEFacet } from './select/cte-facet.js';
68
- import { SelectSetOpFacet } from './select/setop-facet.js';
69
- import { SelectRelationFacet } from './select/relation-facet.js';
70
-
71
- type ColumnSelectionValue =
72
- | ColumnDef
73
- | FunctionNode
74
- | CaseExpressionNode
75
- | WindowFunctionNode
76
- | TypedExpression<unknown>;
77
-
78
- type SelectionValueType<TValue> =
79
- TValue extends TypedExpression<infer TRuntime> ? TRuntime :
80
- TValue extends ColumnDef ? ColumnToTs<TValue> :
81
- unknown;
82
-
83
- type SelectionResult<TSelection extends Record<string, ColumnSelectionValue>> = {
84
- [K in keyof TSelection]: SelectionValueType<TSelection[K]>;
85
- };
86
-
87
- type SelectionFromKeys<
88
- TTable extends TableDef,
89
- K extends keyof TTable['columns'] & string
90
- > = Pick<InferRow<TTable>, K>;
91
-
92
- type DeepSelectEntry<TTable extends TableDef> = {
93
- type: 'root';
94
- columns: (keyof TTable['columns'] & string)[];
95
- } | {
96
- type: 'relation';
97
- relationName: keyof TTable['relations'] & string;
98
- columns: string[];
99
- };
100
-
101
- type DeepSelectConfig<TTable extends TableDef> = DeepSelectEntry<TTable>[];
102
-
103
- /**
104
- * Main query builder class for constructing SQL SELECT queries
105
- * @typeParam T - Result type for projections (unused)
106
- * @typeParam TTable - Table definition being queried
107
- */
108
- export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends TableDef = TableDef> {
109
- private readonly env: SelectQueryBuilderEnvironment;
110
- private readonly context: SelectQueryBuilderContext;
111
- private readonly columnSelector: ColumnSelector;
112
- private readonly fromFacet: SelectFromFacet;
113
- private readonly joinFacet: SelectJoinFacet;
114
- private readonly projectionFacet: SelectProjectionFacet;
115
- private readonly predicateFacet: SelectPredicateFacet;
116
- private readonly cteFacet: SelectCTEFacet;
117
- private readonly setOpFacet: SelectSetOpFacet;
118
- private readonly relationFacet: SelectRelationFacet;
119
- private readonly lazyRelations: Set<string>;
120
- private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
121
- private readonly entityConstructor?: EntityConstructor;
122
- private readonly includeTree: NormalizedRelationIncludeTree;
123
-
124
- /**
125
- * Creates a new SelectQueryBuilder instance
126
- * @param table - Table definition to query
127
- * @param state - Optional initial query state
128
- * @param hydration - Optional hydration manager
129
- * @param dependencies - Optional query builder dependencies
130
- */
131
- constructor(
132
- table: TTable,
133
- state?: SelectQueryState,
134
- hydration?: HydrationManager,
135
- dependencies?: Partial<SelectQueryBuilderDependencies>,
136
- lazyRelations?: Set<string>,
137
- lazyRelationOptions?: Map<string, RelationIncludeOptions>,
138
- entityConstructor?: EntityConstructor,
139
- includeTree?: NormalizedRelationIncludeTree
140
- ) {
141
- const deps = resolveSelectQueryBuilderDependencies(dependencies);
142
- this.env = { table, deps };
143
- const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
144
- const initialState = state ?? deps.createState(table);
145
- const initialHydration = hydration ?? deps.createHydration(table);
146
- this.context = {
147
- state: initialState,
148
- hydration: initialHydration
149
- };
150
- this.lazyRelations = new Set(lazyRelations ?? []);
151
- this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
152
- this.entityConstructor = entityConstructor;
153
- this.includeTree = includeTree ?? {};
154
- this.columnSelector = deps.createColumnSelector(this.env);
155
- const relationManager = deps.createRelationManager(this.env);
156
- this.fromFacet = new SelectFromFacet(this.env, createAstService);
157
- this.joinFacet = new SelectJoinFacet(this.env, createAstService);
158
- this.projectionFacet = new SelectProjectionFacet(this.columnSelector);
159
- this.predicateFacet = new SelectPredicateFacet(this.env, createAstService);
160
- this.cteFacet = new SelectCTEFacet(this.env, createAstService);
161
- this.setOpFacet = new SelectSetOpFacet(this.env, createAstService);
162
- this.relationFacet = new SelectRelationFacet(relationManager);
163
- }
164
-
165
- /**
166
- * Creates a new SelectQueryBuilder instance with updated context and lazy relations
167
- * @param context - Updated query context
168
- * @param lazyRelations - Updated lazy relations set
169
- * @returns New SelectQueryBuilder instance
170
- */
171
- private clone<TNext = T>(
172
- context: SelectQueryBuilderContext = this.context,
173
- lazyRelations = new Set(this.lazyRelations),
174
- lazyRelationOptions = new Map(this.lazyRelationOptions),
175
- includeTree = this.includeTree
176
- ): SelectQueryBuilder<TNext, TTable> {
177
- return new SelectQueryBuilder(
178
- this.env.table as TTable,
179
- context.state,
180
- context.hydration,
181
- this.env.deps,
182
- lazyRelations,
183
- lazyRelationOptions,
184
- this.entityConstructor,
185
- includeTree
186
- ) as SelectQueryBuilder<TNext, TTable>;
187
- }
188
-
189
- /**
190
- * Applies an alias to the root FROM table.
191
- * @param alias - Alias to apply
192
- * @example
193
- * const qb = new SelectQueryBuilder(userTable).as('u');
194
- */
195
- as(alias: string): SelectQueryBuilder<T, TTable> {
196
- const nextContext = this.fromFacet.as(this.context, alias);
197
- return this.clone(nextContext);
198
- }
199
-
200
- /**
201
- * Applies correlation expression to the query AST
202
- * @param ast - Query AST to modify
203
- * @param correlation - Correlation expression
204
- * @returns Modified AST with correlation applied
205
- */
206
- private applyCorrelation(ast: SelectQueryNode, correlation?: ExpressionNode): SelectQueryNode {
207
- if (!correlation) return ast;
208
- const combinedWhere = ast.where ? and(correlation, ast.where) : correlation;
209
- return {
210
- ...ast,
211
- where: combinedWhere
212
- };
213
- }
214
-
215
- /**
216
- * Creates a new child query builder for a related table
217
- * @param table - Table definition for the child builder
218
- * @returns New SelectQueryBuilder instance for the child table
219
- */
220
- private createChildBuilder<R, TChild extends TableDef>(table: TChild): SelectQueryBuilder<R, TChild> {
221
- return new SelectQueryBuilder(table, undefined, undefined, this.env.deps);
222
- }
223
-
224
- /**
225
- * Applies a set operation to the query
226
- * @param operator - Set operation kind
227
- * @param query - Query to combine with
228
- * @returns Updated query context with set operation
229
- */
230
- private applySetOperation<TSub extends TableDef>(
231
- operator: SetOperationKind,
232
- query: SelectQueryBuilder<T, TSub> | SelectQueryNode
233
-
234
- ): SelectQueryBuilderContext {
235
- const subAst = resolveSelectQuery(query);
236
- return this.setOpFacet.applySetOperation(this.context, operator, subAst);
237
- }
238
-
239
- /**
240
- * Selects columns for the query (unified overloaded method).
241
- * Can be called with column names or a projection object.
242
- * @param args - Column names or projection object
243
- * @returns New query builder instance with selected columns
244
- * @example
245
- * // Select specific columns
246
- * qb.select('id', 'name', 'email');
247
- * @example
248
- * // Select with aliases and expressions
249
- * qb.select({
250
- * id: userTable.columns.id,
251
- * fullName: concat(userTable.columns.firstName, ' ', userTable.columns.lastName)
252
- * });
253
- */
254
- select<K extends keyof TTable['columns'] & string>(
255
- ...args: K[]
256
- ): SelectQueryBuilder<T & SelectionFromKeys<TTable, K>, TTable>;
257
- select<TSelection extends Record<string, ColumnSelectionValue>>(
258
- columns: TSelection
259
- ): SelectQueryBuilder<T & SelectionResult<TSelection>, TTable>;
260
- select<
261
- K extends keyof TTable['columns'] & string,
262
- TSelection extends Record<string, ColumnSelectionValue>
263
- >(
264
- ...args: K[] | [TSelection]
265
- ): SelectQueryBuilder<T, TTable> {
266
- // If first arg is an object (not a string), treat as projection map
267
- if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
268
- const columns = args[0] as TSelection;
269
- return this.clone<T & SelectionResult<TSelection>>(
270
- this.projectionFacet.select(this.context, columns)
271
- );
272
- }
273
-
274
- // Otherwise, treat as column names
275
- const cols = args as K[];
276
- const selection: Record<string, ColumnDef> = {};
277
- for (const key of cols) {
278
- const col = this.env.table.columns[key];
279
- if (!col) {
280
- throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
281
- }
282
- selection[key] = col;
283
- }
284
-
285
- return this.clone<T & SelectionFromKeys<TTable, K>>(
286
- this.projectionFacet.select(this.context, selection)
287
- );
288
- }
289
-
290
- /**
291
- * Selects raw column expressions
292
- * @param cols - Column expressions as strings
293
- * @returns New query builder instance with raw column selections
294
- * @example
295
- * qb.selectRaw('COUNT(*) as total', 'UPPER(name) as upper_name');
296
- */
297
- selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
298
- return this.clone(this.projectionFacet.selectRaw(this.context, cols));
299
- }
300
-
301
- /**
302
- * Adds a Common Table Expression (CTE) to the query
303
- * @param name - Name of the CTE
304
- * @param query - Query builder or query node for the CTE
305
- * @param columns - Optional column names for the CTE
306
- * @returns New query builder instance with the CTE
307
- * @example
308
- * const recentUsers = new SelectQueryBuilder(userTable)
309
- * .where(gt(userTable.columns.createdAt, subDays(now(), 30)));
310
- * const qb = new SelectQueryBuilder(userTable)
311
- * .with('recent_users', recentUsers)
312
- * .from('recent_users');
313
- */
314
- with<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
315
- const subAst = resolveSelectQuery(query);
316
- const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, false);
317
- return this.clone(nextContext);
318
- }
319
-
320
- /**
321
- * Adds a recursive Common Table Expression (CTE) to the query
322
- * @param name - Name of the CTE
323
- * @param query - Query builder or query node for the CTE
324
- * @param columns - Optional column names for the CTE
325
- * @returns New query builder instance with the recursive CTE
326
- * @example
327
- * // Base case: select root nodes
328
- * const baseQuery = new SelectQueryBuilder(orgTable)
329
- * .where(eq(orgTable.columns.parentId, 1));
330
- * // Recursive case: join with the CTE itself
331
- * const recursiveQuery = new SelectQueryBuilder(orgTable)
332
- * .join('org_hierarchy', 'oh', eq(orgTable.columns.parentId, col('oh.id')));
333
- * // Combine base and recursive parts
334
- * const orgHierarchy = baseQuery.union(recursiveQuery);
335
- * // Use in main query
336
- * const qb = new SelectQueryBuilder(orgTable)
337
- * .withRecursive('org_hierarchy', orgHierarchy)
338
- * .from('org_hierarchy');
339
- */
340
- withRecursive<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
341
- const subAst = resolveSelectQuery(query);
342
- const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, true);
343
- return this.clone(nextContext);
344
- }
345
-
346
- /**
347
- * Replaces the FROM clause with a derived table (subquery with alias)
348
- * @param subquery - Subquery to use as the FROM source
349
- * @param alias - Alias for the derived table
350
- * @param columnAliases - Optional column alias list
351
- * @returns New query builder instance with updated FROM
352
- * @example
353
- * const subquery = new SelectQueryBuilder(userTable)
354
- * .select('id', 'name')
355
- * .where(gt(userTable.columns.score, 100));
356
- * qb.fromSubquery(subquery, 'high_scorers', ['userId', 'userName']);
357
- */
358
- fromSubquery<TSub extends TableDef>(
359
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
360
- alias: string,
361
- columnAliases?: string[]
362
- ): SelectQueryBuilder<T, TTable> {
363
- const subAst = resolveSelectQuery(subquery);
364
- const nextContext = this.fromFacet.fromSubquery(this.context, subAst, alias, columnAliases);
365
- return this.clone(nextContext);
366
- }
367
-
368
- /**
369
- * Replaces the FROM clause with a function table expression.
370
- * @param name - Function name
371
- * @param args - Optional function arguments
372
- * @param alias - Optional alias for the function table
373
- * @param options - Optional function-table metadata (lateral, ordinality, column aliases, schema)
374
- * @example
375
- * qb.fromFunctionTable(
376
- * 'generate_series',
377
- * [literal(1), literal(10), literal(1)],
378
- * 'series',
379
- * { columnAliases: ['value'] }
380
- * );
381
- */
382
- fromFunctionTable(
383
- name: string,
384
- args: OperandNode[] = [],
385
- alias?: string,
386
- options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
387
- ): SelectQueryBuilder<T, TTable> {
388
- const nextContext = this.fromFacet.fromFunctionTable(this.context, name, args, alias, options);
389
- return this.clone(nextContext);
390
- }
391
-
392
- /**
393
- * Selects a subquery as a column
394
- * @param alias - Alias for the subquery column
395
- * @param sub - Query builder or query node for the subquery
396
- * @returns New query builder instance with the subquery selection
397
- * @example
398
- * const postCount = new SelectQueryBuilder(postTable)
399
- * .select(count(postTable.columns.id))
400
- * .where(eq(postTable.columns.userId, col('u.id')));
401
- * qb.select('id', 'name')
402
- * .selectSubquery('postCount', postCount);
403
- */
404
- selectSubquery<TValue = unknown, K extends string = string, TSub extends TableDef = TableDef>(
405
- alias: K,
406
- sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
407
- ): SelectQueryBuilder<T & Record<K, TValue>, TTable> {
408
- const query = resolveSelectQuery(sub);
409
- return this.clone<T & Record<K, TValue>>(
410
- this.projectionFacet.selectSubquery(this.context, alias, query)
411
- );
412
- }
413
-
414
- /**
415
- * Adds a JOIN against a derived table (subquery with alias)
416
- * @param subquery - Subquery to join
417
- * @param alias - Alias for the derived table
418
- * @param condition - Join condition expression
419
- * @param joinKind - Join kind (defaults to INNER)
420
- * @param columnAliases - Optional column alias list for the derived table
421
- * @returns New query builder instance with the derived-table join
422
- * @example
423
- * const activeUsers = new SelectQueryBuilder(userTable)
424
- * .where(eq(userTable.columns.active, true));
425
- * qb.joinSubquery(
426
- * activeUsers,
427
- * 'au',
428
- * eq(col('t.userId'), col('au.id')),
429
- * JOIN_KINDS.LEFT
430
- * );
431
- */
432
- joinSubquery<TSub extends TableDef>(
433
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
434
- alias: string,
435
- condition: BinaryExpressionNode,
436
- joinKind: JoinKind = JOIN_KINDS.INNER,
437
- columnAliases?: string[]
438
- ): SelectQueryBuilder<T, TTable> {
439
- const subAst = resolveSelectQuery(subquery);
440
- const nextContext = this.joinFacet.joinSubquery(this.context, subAst, alias, condition, joinKind, columnAliases);
441
- return this.clone(nextContext);
442
- }
443
-
444
- /**
445
- * Adds a join against a function table (e.g., `generate_series`) using `fnTable` internally.
446
- * @param name - Function name
447
- * @param args - Optional arguments passed to the function
448
- * @param alias - Alias for the function table so columns can be referenced
449
- * @param condition - Join condition expression
450
- * @param joinKind - Kind of join (defaults to INNER)
451
- * @param options - Optional metadata (lateral, ordinality, column aliases, schema)
452
- * @example
453
- * qb.joinFunctionTable(
454
- * 'generate_series',
455
- * [literal(1), literal(10)],
456
- * 'gs',
457
- * eq(col('t.value'), col('gs.value')),
458
- * JOIN_KINDS.INNER,
459
- * { columnAliases: ['value'] }
460
- * );
461
- */
462
- joinFunctionTable(
463
- name: string,
464
- args: OperandNode[] = [],
465
- alias: string,
466
- condition: BinaryExpressionNode,
467
- joinKind: JoinKind = JOIN_KINDS.INNER,
468
- options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
469
- ): SelectQueryBuilder<T, TTable> {
470
- const nextContext = this.joinFacet.joinFunctionTable(this.context, name, args, alias, condition, joinKind, options);
471
- return this.clone(nextContext);
472
- }
473
-
474
- /**
475
- * Adds an INNER JOIN to the query
476
- * @param table - Table to join
477
- * @param condition - Join condition expression
478
- * @returns New query builder instance with the INNER JOIN
479
- * @example
480
- * qb.innerJoin(
481
- * postTable,
482
- * eq(userTable.columns.id, postTable.columns.userId)
483
- * );
484
- */
485
- innerJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
486
- const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
487
- return this.clone(nextContext);
488
- }
489
-
490
- /**
491
- * Adds a LEFT JOIN to the query
492
- * @param table - Table to join
493
- * @param condition - Join condition expression
494
- * @returns New query builder instance with the LEFT JOIN
495
- * @example
496
- * qb.leftJoin(
497
- * postTable,
498
- * eq(userTable.columns.id, postTable.columns.userId)
499
- * );
500
- */
501
- leftJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
502
- const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
503
- return this.clone(nextContext);
504
- }
505
-
506
- /**
507
- * Adds a RIGHT JOIN to the query
508
- * @param table - Table to join
509
- * @param condition - Join condition expression
510
- * @returns New query builder instance with the RIGHT JOIN
511
- * @example
512
- * qb.rightJoin(
513
- * postTable,
514
- * eq(userTable.columns.id, postTable.columns.userId)
515
- * );
516
- */
517
- rightJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
518
- const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
519
- return this.clone(nextContext);
520
- }
521
-
522
- /**
523
- * Matches records based on a relationship
524
- * @param relationName - Name of the relationship to match
525
- * @param predicate - Optional predicate expression
526
- * @returns New query builder instance with the relationship match
527
- * @example
528
- * qb.match('posts', eq(postTable.columns.published, true));
529
- */
530
- match<K extends keyof TTable['relations'] & string>(
531
- relationName: K,
532
- predicate?: ExpressionNode
533
- ): SelectQueryBuilder<T, TTable> {
534
- const nextContext = this.relationFacet.match(this.context, relationName, predicate);
535
- return this.clone(nextContext);
536
- }
537
-
538
- /**
539
- * Joins a related table
540
- * @param relationName - Name of the relationship to join
541
- * @param joinKind - Type of join (defaults to INNER)
542
- * @param extraCondition - Optional additional join condition
543
- * @returns New query builder instance with the relationship join
544
- * @example
545
- * qb.joinRelation('posts', JOIN_KINDS.LEFT);
546
- * @example
547
- * qb.joinRelation('posts', JOIN_KINDS.INNER, eq(postTable.columns.published, true));
548
- */
549
- joinRelation<K extends keyof TTable['relations'] & string>(
550
- relationName: K,
551
- joinKind: JoinKind = JOIN_KINDS.INNER,
552
- extraCondition?: ExpressionNode
553
- ): SelectQueryBuilder<T, TTable> {
554
- const nextContext = this.relationFacet.joinRelation(this.context, relationName, joinKind, extraCondition);
555
- return this.clone(nextContext);
556
- }
557
-
558
- /**
559
- * Includes related data in the query results
560
- * @param relationName - Name of the relationship to include
561
- * @param options - Optional include options
562
- * @returns New query builder instance with the relationship inclusion
563
- * @example
564
- * qb.include('posts');
565
- * @example
566
- * qb.include('posts', { columns: ['id', 'title', 'published'] });
567
- * @example
568
- * qb.include('posts', {
569
- * columns: ['id', 'title'],
570
- * where: eq(postTable.columns.published, true)
571
- * });
572
- * @example
573
- * qb.include({ posts: { include: { author: true } } });
574
- */
575
- include<K extends keyof TTable['relations'] & string>(
576
- relationName: K,
577
- options?: RelationIncludeNodeInput<TTable['relations'][K]>
578
- ): SelectQueryBuilder<T, TTable>;
579
- include(relations: RelationIncludeInput<TTable>): SelectQueryBuilder<T, TTable>;
580
- include<K extends keyof TTable['relations'] & string>(
581
- relationNameOrRelations: K | RelationIncludeInput<TTable>,
582
- options?: RelationIncludeNodeInput<TTable['relations'][K]>
583
- ): SelectQueryBuilder<T, TTable> {
584
- if (typeof relationNameOrRelations === 'object' && relationNameOrRelations !== null) {
585
- const normalized = normalizeRelationInclude(relationNameOrRelations as RelationIncludeInput<TableDef>);
586
- let nextContext = this.context;
587
- for (const [relationName, node] of Object.entries(normalized)) {
588
- nextContext = this.relationFacet.include(nextContext, relationName, node.options);
589
- }
590
- const nextTree = mergeRelationIncludeTrees(this.includeTree, normalized);
591
- return this.clone(nextContext, undefined, undefined, nextTree);
592
- }
593
-
594
- const relationName = relationNameOrRelations as string;
595
- const normalizedNode = normalizeRelationIncludeNode(options);
596
- const nextContext = this.relationFacet.include(this.context, relationName, normalizedNode.options);
597
- const shouldStore = Boolean(normalizedNode.include || normalizedNode.options);
598
- const nextTree = shouldStore
599
- ? mergeRelationIncludeTrees(this.includeTree, { [relationName]: normalizedNode })
600
- : this.includeTree;
601
- return this.clone(nextContext, undefined, undefined, nextTree);
602
- }
603
-
604
- /**
605
- * Includes a relation lazily in the query results
606
- * @param relationName - Name of the relation to include lazily
607
- * @param options - Optional include options for lazy loading
608
- * @returns New query builder instance with lazy relation inclusion
609
- * @example
610
- * const qb = new SelectQueryBuilder(userTable).includeLazy('posts');
611
- * const users = await qb.execute(session);
612
- * // Access posts later - they will be loaded on demand
613
- * const posts = await users[0].posts;
614
- */
615
- includeLazy<K extends keyof RelationMap<TTable>>(
616
- relationName: K,
617
- options?: TypedRelationIncludeOptions<TTable['relations'][K]>
618
- ): SelectQueryBuilder<T, TTable> {
619
- let nextContext = this.context;
620
- const relation = this.env.table.relations[relationName as string];
621
- if (relation?.type === RelationKinds.BelongsTo) {
622
- const foreignKey = relation.foreignKey;
623
- const fkColumn = this.env.table.columns[foreignKey];
624
- if (fkColumn) {
625
- const hasAlias = nextContext.state.ast.columns.some(col => {
626
- const node = col as { alias?: string; name?: string };
627
- return (node.alias ?? node.name) === foreignKey;
628
- });
629
- if (!hasAlias) {
630
- nextContext = this.columnSelector.select(nextContext, { [foreignKey]: fkColumn });
631
- }
632
- }
633
- }
634
- const nextLazy = new Set(this.lazyRelations);
635
- nextLazy.add(relationName as string);
636
- const nextOptions = new Map(this.lazyRelationOptions);
637
- if (options) {
638
- nextOptions.set(relationName as string, options);
639
- } else {
640
- nextOptions.delete(relationName as string);
641
- }
642
- return this.clone(nextContext, nextLazy, nextOptions);
643
- }
644
-
645
- /**
646
- * Convenience alias for including only specific columns from a relation.
647
- * @example
648
- * qb.includePick('posts', ['id', 'title', 'createdAt']);
649
- */
650
- includePick<
651
- K extends keyof TTable['relations'] & string,
652
- C extends RelationTargetColumns<TTable['relations'][K]>
653
- >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
654
- const options = { columns: cols as readonly C[] } as unknown as RelationIncludeNodeInput<TTable['relations'][K]>;
655
- return this.include(relationName, options);
656
- }
657
-
658
- /**
659
- * Selects columns for the root table and relations from an array of entries
660
- * @param config - Configuration array for deep column selection
661
- * @returns New query builder instance with deep column selections
662
- * @example
663
- * qb.selectColumnsDeep([
664
- * { type: 'root', columns: ['id', 'name'] },
665
- * { type: 'relation', relationName: 'posts', columns: ['id', 'title'] }
666
- * ]);
667
- */
668
- selectColumnsDeep(config: DeepSelectConfig<TTable>): SelectQueryBuilder<T, TTable> {
669
- // eslint-disable-next-line @typescript-eslint/no-this-alias
670
- let currBuilder: SelectQueryBuilder<T, TTable> = this;
671
-
672
- for (const entry of config) {
673
- if (entry.type === 'root') {
674
- currBuilder = currBuilder.select(...entry.columns);
675
- } else {
676
- const options = { columns: entry.columns } as unknown as RelationIncludeNodeInput<TTable['relations'][typeof entry.relationName]>;
677
- currBuilder = currBuilder.include(entry.relationName, options);
678
- }
679
- }
680
-
681
- return currBuilder;
682
- }
683
-
684
- /**
685
- * Gets the list of lazy relations
686
- * @returns Array of lazy relation names
687
- */
688
- getLazyRelations(): (keyof RelationMap<TTable>)[] {
689
- return Array.from(this.lazyRelations) as (keyof RelationMap<TTable>)[];
690
- }
691
-
692
- /**
693
- * Gets lazy relation include options
694
- * @returns Map of relation names to include options
695
- */
696
- getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
697
- return new Map(this.lazyRelationOptions);
698
- }
699
-
700
- /**
701
- * Gets normalized nested include information for runtime preloading.
702
- */
703
- getIncludeTree(): NormalizedRelationIncludeTree {
704
- return cloneRelationIncludeTree(this.includeTree);
705
- }
706
-
707
- /**
708
- * Gets the table definition for this query builder
709
- * @returns Table definition
710
- */
711
- getTable(): TTable {
712
- return this.env.table as TTable;
713
- }
714
-
715
- /**
716
- * Ensures that if no columns are selected, all columns from the table are selected by default.
717
- */
718
- private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
719
- const columns = this.context.state.ast.columns;
720
- if (!columns || columns.length === 0) {
721
- const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
722
- return this.select(...columnKeys);
723
- }
724
- return this;
725
- }
726
-
727
- /**
728
- * Executes the query and returns hydrated results.
729
- * If the builder was created with an entity constructor (e.g. via selectFromEntity),
730
- * this will automatically return fully materialized entity instances.
731
- *
732
- * @param ctx - ORM session context
733
- * @returns Promise of entity instances (or objects if generic T is not an entity)
734
- * @example
735
- * const users = await selectFromEntity(User).execute(session);
736
- * // users is User[]
737
- * users[0] instanceof User; // true
738
- */
739
- async execute(ctx: OrmSession): Promise<T[]> {
740
- if (this.entityConstructor) {
741
- return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
742
- }
743
- const builder = this.ensureDefaultSelection();
744
- return executeHydrated(ctx, builder) as unknown as T[];
745
- }
746
-
747
- /**
748
- * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
749
- * Use this if you want raw data even when using selectFromEntity.
750
- *
751
- * @param ctx - ORM session context
752
- * @returns Promise of plain entity instances
753
- * @example
754
- * const rows = await selectFromEntity(User).executePlain(session);
755
- * // rows is EntityInstance<UserTable>[] (plain objects)
756
- * rows[0] instanceof User; // false
757
- */
758
- async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
759
- const builder = this.ensureDefaultSelection();
760
- const rows = await executeHydratedPlain(ctx, builder);
761
- return rows as EntityInstance<TTable>[];
762
- }
763
-
764
- /**
765
- * Executes the query and returns results as real class instances.
766
- * Unlike execute(), this returns actual instances of the decorated entity class
767
- * with working methods and proper instanceof checks.
768
- * @param entityClass - The entity class constructor
769
- * @param ctx - ORM session context
770
- * @returns Promise of entity class instances
771
- * @example
772
- * const users = await selectFromEntity(User)
773
- * .include('posts')
774
- * .executeAs(User, session);
775
- * users[0] instanceof User; // true!
776
- * users[0].getFullName(); // works!
777
- */
778
- async executeAs<TEntity extends object>(
779
- entityClass: EntityConstructor<TEntity>,
780
- ctx: OrmSession
781
- ): Promise<TEntity[]> {
782
- const builder = this.ensureDefaultSelection();
783
- const results = await executeHydrated(ctx, builder);
784
- return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
785
- }
786
-
787
- /**
788
- * Executes a count query for the current builder without LIMIT/OFFSET clauses.
789
- *
790
- * @example
791
- * const total = await qb.count(session);
792
- */
793
- async count(session: OrmSession): Promise<number> {
794
- return executeCount(this.context, this.env, session);
795
- }
796
-
797
- /**
798
- * Executes the query and returns both the paged items and the total.
799
- *
800
- * @example
801
- * const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
802
- */
803
- async executePaged(
804
- session: OrmSession,
805
- options: { page: number; pageSize: number }
806
- ): Promise<PaginatedResult<T>> {
807
- const builder = this.ensureDefaultSelection();
808
- return executePagedQuery(builder, session, options, sess => builder.count(sess));
809
- }
810
-
811
- /**
812
- * Executes the query with provided execution and hydration contexts
813
- * @param execCtx - Execution context
814
- * @param hydCtx - Hydration context
815
- * @returns Promise of entity instances
816
- * @example
817
- * const execCtx = new ExecutionContext(session);
818
- * const hydCtx = new HydrationContext();
819
- * const users = await qb.executeWithContexts(execCtx, hydCtx);
820
- */
821
- async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
822
- const builder = this.ensureDefaultSelection();
823
- const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
824
- if (this.entityConstructor) {
825
- return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
826
- }
827
- return results as unknown as T[];
828
- }
829
-
830
- /**
831
- * Adds a WHERE condition to the query
832
- * @param expr - Expression for the WHERE clause
833
- * @returns New query builder instance with the WHERE condition
834
- * @example
835
- * qb.where(eq(userTable.columns.id, 1));
836
- * @example
837
- * qb.where(and(
838
- * eq(userTable.columns.active, true),
839
- * gt(userTable.columns.createdAt, subDays(now(), 30))
840
- * ));
841
- */
842
- where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
843
- const nextContext = this.predicateFacet.where(this.context, expr);
844
- return this.clone(nextContext);
845
- }
846
-
847
- /**
848
- * Adds a GROUP BY clause to the query
849
- * @param term - Column definition or ordering term to group by
850
- * @returns New query builder instance with the GROUP BY clause
851
- * @example
852
- * qb.select('departmentId', count(userTable.columns.id))
853
- * .groupBy(userTable.columns.departmentId);
854
- */
855
- groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
856
- const nextContext = this.predicateFacet.groupBy(this.context, term);
857
- return this.clone(nextContext);
858
- }
859
-
860
- /**
861
- * Adds a HAVING condition to the query
862
- * @param expr - Expression for the HAVING clause
863
- * @returns New query builder instance with the HAVING condition
864
- * @example
865
- * qb.select('departmentId', count(userTable.columns.id))
866
- * .groupBy(userTable.columns.departmentId)
867
- * .having(gt(count(userTable.columns.id), 5));
868
- */
869
- having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
870
- const nextContext = this.predicateFacet.having(this.context, expr);
871
- return this.clone(nextContext);
872
- }
873
-
874
- /**
875
- * Adds an ORDER BY clause to the query
876
- * @param term - Column definition or ordering term to order by
877
- * @param directionOrOptions - Order direction or options (defaults to ASC)
878
- * @returns New query builder instance with the ORDER BY clause
879
- *
880
- * @example
881
- * qb.orderBy(userTable.columns.createdAt, 'DESC');
882
- */
883
- orderBy(
884
- term: ColumnDef | OrderingTerm,
885
- directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string } = ORDER_DIRECTIONS.ASC
886
- ): SelectQueryBuilder<T, TTable> {
887
- const nextContext = applyOrderBy(this.context, this.predicateFacet, term, directionOrOptions);
888
-
889
- return this.clone(nextContext);
890
- }
891
-
892
- /**
893
- * Adds a DISTINCT clause to the query
894
- * @param cols - Columns to make distinct
895
- * @returns New query builder instance with the DISTINCT clause
896
- * @example
897
- * qb.distinct(userTable.columns.email);
898
- * @example
899
- * qb.distinct(userTable.columns.firstName, userTable.columns.lastName);
900
- */
901
- distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
902
- return this.clone(this.projectionFacet.distinct(this.context, cols));
903
- }
904
-
905
- /**
906
- * Adds a LIMIT clause to the query
907
- * @param n - Maximum number of rows to return
908
- * @returns New query builder instance with the LIMIT clause
909
- * @example
910
- * qb.limit(10);
911
- * @example
912
- * qb.limit(20).offset(40); // Pagination: page 3 with 20 items per page
913
- */
914
- limit(n: number): SelectQueryBuilder<T, TTable> {
915
- const nextContext = this.predicateFacet.limit(this.context, n);
916
- return this.clone(nextContext);
917
- }
918
-
919
- /**
920
- * Adds an OFFSET clause to the query
921
- * @param n - Number of rows to skip
922
- * @returns New query builder instance with the OFFSET clause
923
- * @example
924
- * qb.offset(10);
925
- * @example
926
- * qb.limit(20).offset(40); // Pagination: page 3 with 20 items per page
927
- */
928
- offset(n: number): SelectQueryBuilder<T, TTable> {
929
- const nextContext = this.predicateFacet.offset(this.context, n);
930
- return this.clone(nextContext);
931
- }
932
-
933
- /**
934
- * Combines this query with another using UNION
935
- * @param query - Query to union with
936
- * @returns New query builder instance with the set operation
937
- * @example
938
- * const activeUsers = new SelectQueryBuilder(userTable)
939
- * .where(eq(userTable.columns.active, true));
940
- * const inactiveUsers = new SelectQueryBuilder(userTable)
941
- * .where(eq(userTable.columns.active, false));
942
- * qb.union(activeUsers).union(inactiveUsers);
943
- */
944
- union<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
945
- return this.clone(this.applySetOperation('UNION', query));
946
- }
947
-
948
- /**
949
- * Combines this query with another using UNION ALL
950
- * @param query - Query to union with
951
- * @returns New query builder instance with the set operation
952
- * @example
953
- * const q1 = new SelectQueryBuilder(userTable).where(gt(userTable.columns.score, 80));
954
- * const q2 = new SelectQueryBuilder(userTable).where(lt(userTable.columns.score, 20));
955
- * qb.unionAll(q1).unionAll(q2);
956
- */
957
- unionAll<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
958
- return this.clone(this.applySetOperation('UNION ALL', query));
959
- }
960
-
961
- /**
962
- * Combines this query with another using INTERSECT
963
- * @param query - Query to intersect with
964
- * @returns New query builder instance with the set operation
965
- * @example
966
- * const activeUsers = new SelectQueryBuilder(userTable)
967
- * .where(eq(userTable.columns.active, true));
968
- * const premiumUsers = new SelectQueryBuilder(userTable)
969
- * .where(eq(userTable.columns.premium, true));
970
- * qb.intersect(activeUsers).intersect(premiumUsers);
971
- */
972
- intersect<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
973
- return this.clone(this.applySetOperation('INTERSECT', query));
974
- }
975
-
976
- /**
977
- * Combines this query with another using EXCEPT
978
- * @param query - Query to subtract
979
- * @returns New query builder instance with the set operation
980
- * @example
981
- * const allUsers = new SelectQueryBuilder(userTable);
982
- * const inactiveUsers = new SelectQueryBuilder(userTable)
983
- * .where(eq(userTable.columns.active, false));
984
- * qb.except(allUsers).except(inactiveUsers); // Only active users
985
- */
986
- except<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
987
- return this.clone(this.applySetOperation('EXCEPT', query));
988
- }
989
-
990
- /**
991
- * Adds a WHERE EXISTS condition to the query
992
- * @param subquery - Subquery to check for existence
993
- * @returns New query builder instance with the WHERE EXISTS condition
994
- * @example
995
- * const postsQuery = new SelectQueryBuilder(postTable)
996
- * .where(eq(postTable.columns.userId, col('u.id')));
997
- * qb.whereExists(postsQuery);
998
- */
999
- whereExists<TSub extends TableDef>(
1000
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
1001
- correlate?: ExpressionNode
1002
- ): SelectQueryBuilder<T, TTable> {
1003
- const subAst = resolveSelectQuery(subquery);
1004
- const correlated = this.applyCorrelation(subAst, correlate);
1005
- return this.where(exists(correlated));
1006
- }
1007
-
1008
- /**
1009
- * Adds a WHERE NOT EXISTS condition to the query
1010
- * @param subquery - Subquery to check for non-existence
1011
- * @returns New query builder instance with the WHERE NOT EXISTS condition
1012
- * @example
1013
- * const postsQuery = new SelectQueryBuilder(postTable)
1014
- * .where(eq(postTable.columns.userId, col('u.id')));
1015
- * qb.whereNotExists(postsQuery); // Users without posts
1016
- */
1017
- whereNotExists<TSub extends TableDef>(
1018
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
1019
- correlate?: ExpressionNode
1020
- ): SelectQueryBuilder<T, TTable> {
1021
- const subAst = resolveSelectQuery(subquery);
1022
- const correlated = this.applyCorrelation(subAst, correlate);
1023
- return this.where(notExists(correlated));
1024
- }
1025
-
1026
- /**
1027
- * Adds a WHERE EXISTS condition based on a relationship
1028
- * @param relationName - Name of the relationship to check
1029
- * @param callback - Optional callback to modify the relationship query
1030
- * @returns New query builder instance with the relationship existence check
1031
- *
1032
- * @example
1033
- * qb.whereHas('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
1034
- */
1035
- whereHas<K extends keyof TTable['relations'] & string>(
1036
- relationName: K,
1037
- callbackOrOptions?: RelationCallback | WhereHasOptions,
1038
- maybeOptions?: WhereHasOptions
1039
- ): SelectQueryBuilder<T, TTable> {
1040
- const predicate = buildWhereHasPredicate(
1041
- this.env,
1042
- this.context,
1043
- this.relationFacet,
1044
- table => this.createChildBuilder(table),
1045
- relationName,
1046
- callbackOrOptions,
1047
- maybeOptions,
1048
- false
1049
- );
1050
-
1051
- return this.where(predicate);
1052
- }
1053
-
1054
- /**
1055
- * Adds a WHERE NOT EXISTS condition based on a relationship
1056
- * @param relationName - Name of the relationship to check
1057
- * @param callback - Optional callback to modify the relationship query
1058
- * @returns New query builder instance with the relationship non-existence check
1059
- *
1060
- * @example
1061
- * qb.whereHasNot('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
1062
- */
1063
- whereHasNot<K extends keyof TTable['relations'] & string>(
1064
- relationName: K,
1065
- callbackOrOptions?: RelationCallback | WhereHasOptions,
1066
- maybeOptions?: WhereHasOptions
1067
- ): SelectQueryBuilder<T, TTable> {
1068
- const predicate = buildWhereHasPredicate(
1069
- this.env,
1070
- this.context,
1071
- this.relationFacet,
1072
- table => this.createChildBuilder(table),
1073
- relationName,
1074
- callbackOrOptions,
1075
- maybeOptions,
1076
- true
1077
- );
1078
-
1079
- return this.where(predicate);
1080
- }
1081
-
1082
- /**
1083
- * Compiles the query to SQL for a specific dialect
1084
- * @param dialect - Database dialect to compile for
1085
- * @returns Compiled query with SQL and parameters
1086
- * @example
1087
- * const compiled = qb.select('id', 'name')
1088
- * .where(eq(userTable.columns.active, true))
1089
- * .compile('postgres');
1090
- * console.log(compiled.sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
1091
- */
1092
- compile(dialect: SelectDialectInput): CompiledQuery {
1093
- const resolved = resolveDialectInput(dialect);
1094
- return resolved.compileSelect(this.getAST());
1095
- }
1096
-
1097
- /**
1098
- * Converts the query to SQL string for a specific dialect
1099
- * @param dialect - Database dialect to generate SQL for
1100
- * @returns SQL string representation of the query
1101
- * @example
1102
- * const sql = qb.select('id', 'name')
1103
- * .where(eq(userTable.columns.active, true))
1104
- * .toSql('postgres');
1105
- * console.log(sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
1106
- */
1107
- toSql(dialect: SelectDialectInput): string {
1108
- return this.compile(dialect).sql;
1109
- }
1110
-
1111
- /**
1112
- * Gets the hydration plan for the query
1113
- * @returns Hydration plan or undefined if none exists
1114
- * @example
1115
- * const plan = qb.include('posts').getHydrationPlan();
1116
- * console.log(plan?.relations); // Information about included relations
1117
- */
1118
- getHydrationPlan(): HydrationPlan | undefined {
1119
- return this.context.hydration.getPlan();
1120
- }
1121
-
1122
- /**
1123
- * Gets the Abstract Syntax Tree (AST) representation of the query
1124
- * @returns Query AST with hydration applied
1125
- * @example
1126
- * const ast = qb.select('id', 'name').getAST();
1127
- * console.log(ast.columns); // Array of column nodes
1128
- * console.log(ast.from); // From clause information
1129
- */
1130
- getAST(): SelectQueryNode {
1131
- return this.context.hydration.applyToAst(this.context.state.ast);
1132
- }
1133
- }
1
+ import { TableDef } from '../schema/table.js';
2
+ import { ColumnDef } from '../schema/column-types.js';
3
+ import { OrderingTerm, SelectQueryNode, SetOperationKind } from '../core/ast/query.js';
4
+ import { HydrationPlan } from '../core/hydration/types.js';
5
+ import {
6
+ ColumnNode,
7
+ ExpressionNode,
8
+ FunctionNode,
9
+ BinaryExpressionNode,
10
+ CaseExpressionNode,
11
+ WindowFunctionNode,
12
+ and,
13
+ exists,
14
+ notExists,
15
+ OperandNode
16
+ } from '../core/ast/expression.js';
17
+ import type { TypedExpression } from '../core/ast/expression.js';
18
+ import { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
19
+ import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
20
+
21
+ type SelectDialectInput = Dialect | DialectKey;
22
+
23
+ import { SelectQueryState } from './select-query-state.js';
24
+ import { HydrationManager } from './hydration-manager.js';
25
+ import {
26
+ resolveSelectQueryBuilderDependencies,
27
+ SelectQueryBuilderContext,
28
+ SelectQueryBuilderDependencies,
29
+ SelectQueryBuilderEnvironment
30
+ } from './select-query-builder-deps.js';
31
+ import { ColumnSelector } from './column-selector.js';
32
+ import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
33
+ import { RelationKinds } from '../schema/relation.js';
34
+ import {
35
+ RelationIncludeInput,
36
+ RelationIncludeNodeInput,
37
+ NormalizedRelationIncludeTree,
38
+ cloneRelationIncludeTree,
39
+ mergeRelationIncludeTrees,
40
+ normalizeRelationInclude,
41
+ normalizeRelationIncludeNode
42
+ } from './relation-include-tree.js';
43
+ import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
44
+ import { EntityInstance, RelationMap } from '../schema/types.js';
45
+ import type { ColumnToTs, InferRow } from '../schema/types.js';
46
+ import { OrmSession } from '../orm/orm-session.ts';
47
+ import { ExecutionContext } from '../orm/execution-context.js';
48
+ import { HydrationContext } from '../orm/hydration-context.js';
49
+ import { executeHydrated, executeHydratedPlain, executeHydratedWithContexts } from '../orm/execute.js';
50
+ import { EntityConstructor } from '../orm/entity-metadata.js';
51
+ import { materializeAs } from '../orm/entity-materializer.js';
52
+ import { resolveSelectQuery } from './query-resolution.js';
53
+ import {
54
+ applyOrderBy,
55
+ buildWhereHasPredicate,
56
+ executeCount,
57
+ executePagedQuery,
58
+ PaginatedResult,
59
+ RelationCallback,
60
+ WhereHasOptions
61
+ } from './select/select-operations.js';
62
+ export type { PaginatedResult };
63
+ import { SelectFromFacet } from './select/from-facet.js';
64
+ import { SelectJoinFacet } from './select/join-facet.js';
65
+ import { SelectProjectionFacet } from './select/projection-facet.js';
66
+ import { SelectPredicateFacet } from './select/predicate-facet.js';
67
+ import { SelectCTEFacet } from './select/cte-facet.js';
68
+ import { SelectSetOpFacet } from './select/setop-facet.js';
69
+ import { SelectRelationFacet } from './select/relation-facet.js';
70
+
71
+ type ColumnSelectionValue =
72
+ | ColumnDef
73
+ | FunctionNode
74
+ | CaseExpressionNode
75
+ | WindowFunctionNode
76
+ | TypedExpression<unknown>;
77
+
78
+ type SelectionValueType<TValue> =
79
+ TValue extends TypedExpression<infer TRuntime> ? TRuntime :
80
+ TValue extends ColumnDef ? ColumnToTs<TValue> :
81
+ unknown;
82
+
83
+ type SelectionResult<TSelection extends Record<string, ColumnSelectionValue>> = {
84
+ [K in keyof TSelection]: SelectionValueType<TSelection[K]>;
85
+ };
86
+
87
+ type SelectionFromKeys<
88
+ TTable extends TableDef,
89
+ K extends keyof TTable['columns'] & string
90
+ > = Pick<InferRow<TTable>, K>;
91
+
92
+ type DeepSelectEntry<TTable extends TableDef> = {
93
+ type: 'root';
94
+ columns: (keyof TTable['columns'] & string)[];
95
+ } | {
96
+ type: 'relation';
97
+ relationName: keyof TTable['relations'] & string;
98
+ columns: string[];
99
+ };
100
+
101
+ type DeepSelectConfig<TTable extends TableDef> = DeepSelectEntry<TTable>[];
102
+
103
+ /**
104
+ * Main query builder class for constructing SQL SELECT queries
105
+ * @typeParam T - Result type for projections (unused)
106
+ * @typeParam TTable - Table definition being queried
107
+ */
108
+ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends TableDef = TableDef> {
109
+ private readonly env: SelectQueryBuilderEnvironment;
110
+ private readonly context: SelectQueryBuilderContext;
111
+ private readonly columnSelector: ColumnSelector;
112
+ private readonly fromFacet: SelectFromFacet;
113
+ private readonly joinFacet: SelectJoinFacet;
114
+ private readonly projectionFacet: SelectProjectionFacet;
115
+ private readonly predicateFacet: SelectPredicateFacet;
116
+ private readonly cteFacet: SelectCTEFacet;
117
+ private readonly setOpFacet: SelectSetOpFacet;
118
+ private readonly relationFacet: SelectRelationFacet;
119
+ private readonly lazyRelations: Set<string>;
120
+ private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
121
+ private readonly entityConstructor?: EntityConstructor;
122
+ private readonly includeTree: NormalizedRelationIncludeTree;
123
+
124
+ /**
125
+ * Creates a new SelectQueryBuilder instance
126
+ * @param table - Table definition to query
127
+ * @param state - Optional initial query state
128
+ * @param hydration - Optional hydration manager
129
+ * @param dependencies - Optional query builder dependencies
130
+ */
131
+ constructor(
132
+ table: TTable,
133
+ state?: SelectQueryState,
134
+ hydration?: HydrationManager,
135
+ dependencies?: Partial<SelectQueryBuilderDependencies>,
136
+ lazyRelations?: Set<string>,
137
+ lazyRelationOptions?: Map<string, RelationIncludeOptions>,
138
+ entityConstructor?: EntityConstructor,
139
+ includeTree?: NormalizedRelationIncludeTree
140
+ ) {
141
+ const deps = resolveSelectQueryBuilderDependencies(dependencies);
142
+ this.env = { table, deps };
143
+ const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
144
+ const initialState = state ?? deps.createState(table);
145
+ const initialHydration = hydration ?? deps.createHydration(table);
146
+ this.context = {
147
+ state: initialState,
148
+ hydration: initialHydration
149
+ };
150
+ this.lazyRelations = new Set(lazyRelations ?? []);
151
+ this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
152
+ this.entityConstructor = entityConstructor;
153
+ this.includeTree = includeTree ?? {};
154
+ this.columnSelector = deps.createColumnSelector(this.env);
155
+ const relationManager = deps.createRelationManager(this.env);
156
+ this.fromFacet = new SelectFromFacet(this.env, createAstService);
157
+ this.joinFacet = new SelectJoinFacet(this.env, createAstService);
158
+ this.projectionFacet = new SelectProjectionFacet(this.columnSelector);
159
+ this.predicateFacet = new SelectPredicateFacet(this.env, createAstService);
160
+ this.cteFacet = new SelectCTEFacet(this.env, createAstService);
161
+ this.setOpFacet = new SelectSetOpFacet(this.env, createAstService);
162
+ this.relationFacet = new SelectRelationFacet(relationManager);
163
+ }
164
+
165
+ /**
166
+ * Creates a new SelectQueryBuilder instance with updated context and lazy relations
167
+ * @param context - Updated query context
168
+ * @param lazyRelations - Updated lazy relations set
169
+ * @returns New SelectQueryBuilder instance
170
+ */
171
+ private clone<TNext = T>(
172
+ context: SelectQueryBuilderContext = this.context,
173
+ lazyRelations = new Set(this.lazyRelations),
174
+ lazyRelationOptions = new Map(this.lazyRelationOptions),
175
+ includeTree = this.includeTree
176
+ ): SelectQueryBuilder<TNext, TTable> {
177
+ return new SelectQueryBuilder(
178
+ this.env.table as TTable,
179
+ context.state,
180
+ context.hydration,
181
+ this.env.deps,
182
+ lazyRelations,
183
+ lazyRelationOptions,
184
+ this.entityConstructor,
185
+ includeTree
186
+ ) as SelectQueryBuilder<TNext, TTable>;
187
+ }
188
+
189
+ /**
190
+ * Applies an alias to the root FROM table.
191
+ * @param alias - Alias to apply
192
+ * @example
193
+ * const qb = new SelectQueryBuilder(userTable).as('u');
194
+ */
195
+ as(alias: string): SelectQueryBuilder<T, TTable> {
196
+ const nextContext = this.fromFacet.as(this.context, alias);
197
+ return this.clone(nextContext);
198
+ }
199
+
200
+ /**
201
+ * Applies correlation expression to the query AST
202
+ * @param ast - Query AST to modify
203
+ * @param correlation - Correlation expression
204
+ * @returns Modified AST with correlation applied
205
+ */
206
+ private applyCorrelation(ast: SelectQueryNode, correlation?: ExpressionNode): SelectQueryNode {
207
+ if (!correlation) return ast;
208
+ const combinedWhere = ast.where ? and(correlation, ast.where) : correlation;
209
+ return {
210
+ ...ast,
211
+ where: combinedWhere
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Creates a new child query builder for a related table
217
+ * @param table - Table definition for the child builder
218
+ * @returns New SelectQueryBuilder instance for the child table
219
+ */
220
+ private createChildBuilder<R, TChild extends TableDef>(table: TChild): SelectQueryBuilder<R, TChild> {
221
+ return new SelectQueryBuilder(table, undefined, undefined, this.env.deps);
222
+ }
223
+
224
+ /**
225
+ * Applies a set operation to the query
226
+ * @param operator - Set operation kind
227
+ * @param query - Query to combine with
228
+ * @returns Updated query context with set operation
229
+ */
230
+ private applySetOperation<TSub extends TableDef>(
231
+ operator: SetOperationKind,
232
+ query: SelectQueryBuilder<T, TSub> | SelectQueryNode
233
+
234
+ ): SelectQueryBuilderContext {
235
+ const subAst = resolveSelectQuery(query);
236
+ return this.setOpFacet.applySetOperation(this.context, operator, subAst);
237
+ }
238
+
239
+ /**
240
+ * Selects columns for the query (unified overloaded method).
241
+ * Can be called with column names or a projection object.
242
+ * @param args - Column names or projection object
243
+ * @returns New query builder instance with selected columns
244
+ * @example
245
+ * // Select specific columns
246
+ * qb.select('id', 'name', 'email');
247
+ * @example
248
+ * // Select with aliases and expressions
249
+ * qb.select({
250
+ * id: userTable.columns.id,
251
+ * fullName: concat(userTable.columns.firstName, ' ', userTable.columns.lastName)
252
+ * });
253
+ */
254
+ select<K extends keyof TTable['columns'] & string>(
255
+ ...args: K[]
256
+ ): SelectQueryBuilder<T & SelectionFromKeys<TTable, K>, TTable>;
257
+ select<TSelection extends Record<string, ColumnSelectionValue>>(
258
+ columns: TSelection
259
+ ): SelectQueryBuilder<T & SelectionResult<TSelection>, TTable>;
260
+ select<
261
+ K extends keyof TTable['columns'] & string,
262
+ TSelection extends Record<string, ColumnSelectionValue>
263
+ >(
264
+ ...args: K[] | [TSelection]
265
+ ): SelectQueryBuilder<T, TTable> {
266
+ // If first arg is an object (not a string), treat as projection map
267
+ if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
268
+ const columns = args[0] as TSelection;
269
+ return this.clone<T & SelectionResult<TSelection>>(
270
+ this.projectionFacet.select(this.context, columns)
271
+ );
272
+ }
273
+
274
+ // Otherwise, treat as column names
275
+ const cols = args as K[];
276
+ const selection: Record<string, ColumnDef> = {};
277
+ for (const key of cols) {
278
+ const col = this.env.table.columns[key];
279
+ if (!col) {
280
+ throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
281
+ }
282
+ selection[key] = col;
283
+ }
284
+
285
+ return this.clone<T & SelectionFromKeys<TTable, K>>(
286
+ this.projectionFacet.select(this.context, selection)
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Selects raw column expressions
292
+ * @param cols - Column expressions as strings
293
+ * @returns New query builder instance with raw column selections
294
+ * @example
295
+ * qb.selectRaw('COUNT(*) as total', 'UPPER(name) as upper_name');
296
+ */
297
+ selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
298
+ return this.clone(this.projectionFacet.selectRaw(this.context, cols));
299
+ }
300
+
301
+ /**
302
+ * Adds a Common Table Expression (CTE) to the query
303
+ * @param name - Name of the CTE
304
+ * @param query - Query builder or query node for the CTE
305
+ * @param columns - Optional column names for the CTE
306
+ * @returns New query builder instance with the CTE
307
+ * @example
308
+ * const recentUsers = new SelectQueryBuilder(userTable)
309
+ * .where(gt(userTable.columns.createdAt, subDays(now(), 30)));
310
+ * const qb = new SelectQueryBuilder(userTable)
311
+ * .with('recent_users', recentUsers)
312
+ * .from('recent_users');
313
+ */
314
+ with<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
315
+ const subAst = resolveSelectQuery(query);
316
+ const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, false);
317
+ return this.clone(nextContext);
318
+ }
319
+
320
+ /**
321
+ * Adds a recursive Common Table Expression (CTE) to the query
322
+ * @param name - Name of the CTE
323
+ * @param query - Query builder or query node for the CTE
324
+ * @param columns - Optional column names for the CTE
325
+ * @returns New query builder instance with the recursive CTE
326
+ * @example
327
+ * // Base case: select root nodes
328
+ * const baseQuery = new SelectQueryBuilder(orgTable)
329
+ * .where(eq(orgTable.columns.parentId, 1));
330
+ * // Recursive case: join with the CTE itself
331
+ * const recursiveQuery = new SelectQueryBuilder(orgTable)
332
+ * .join('org_hierarchy', 'oh', eq(orgTable.columns.parentId, col('oh.id')));
333
+ * // Combine base and recursive parts
334
+ * const orgHierarchy = baseQuery.union(recursiveQuery);
335
+ * // Use in main query
336
+ * const qb = new SelectQueryBuilder(orgTable)
337
+ * .withRecursive('org_hierarchy', orgHierarchy)
338
+ * .from('org_hierarchy');
339
+ */
340
+ withRecursive<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
341
+ const subAst = resolveSelectQuery(query);
342
+ const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, true);
343
+ return this.clone(nextContext);
344
+ }
345
+
346
+ /**
347
+ * Replaces the FROM clause with a derived table (subquery with alias)
348
+ * @param subquery - Subquery to use as the FROM source
349
+ * @param alias - Alias for the derived table
350
+ * @param columnAliases - Optional column alias list
351
+ * @returns New query builder instance with updated FROM
352
+ * @example
353
+ * const subquery = new SelectQueryBuilder(userTable)
354
+ * .select('id', 'name')
355
+ * .where(gt(userTable.columns.score, 100));
356
+ * qb.fromSubquery(subquery, 'high_scorers', ['userId', 'userName']);
357
+ */
358
+ fromSubquery<TSub extends TableDef>(
359
+ subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
360
+ alias: string,
361
+ columnAliases?: string[]
362
+ ): SelectQueryBuilder<T, TTable> {
363
+ const subAst = resolveSelectQuery(subquery);
364
+ const nextContext = this.fromFacet.fromSubquery(this.context, subAst, alias, columnAliases);
365
+ return this.clone(nextContext);
366
+ }
367
+
368
+ /**
369
+ * Replaces the FROM clause with a function table expression.
370
+ * @param name - Function name
371
+ * @param args - Optional function arguments
372
+ * @param alias - Optional alias for the function table
373
+ * @param options - Optional function-table metadata (lateral, ordinality, column aliases, schema)
374
+ * @example
375
+ * qb.fromFunctionTable(
376
+ * 'generate_series',
377
+ * [literal(1), literal(10), literal(1)],
378
+ * 'series',
379
+ * { columnAliases: ['value'] }
380
+ * );
381
+ */
382
+ fromFunctionTable(
383
+ name: string,
384
+ args: OperandNode[] = [],
385
+ alias?: string,
386
+ options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
387
+ ): SelectQueryBuilder<T, TTable> {
388
+ const nextContext = this.fromFacet.fromFunctionTable(this.context, name, args, alias, options);
389
+ return this.clone(nextContext);
390
+ }
391
+
392
+ /**
393
+ * Selects a subquery as a column
394
+ * @param alias - Alias for the subquery column
395
+ * @param sub - Query builder or query node for the subquery
396
+ * @returns New query builder instance with the subquery selection
397
+ * @example
398
+ * const postCount = new SelectQueryBuilder(postTable)
399
+ * .select(count(postTable.columns.id))
400
+ * .where(eq(postTable.columns.userId, col('u.id')));
401
+ * qb.select('id', 'name')
402
+ * .selectSubquery('postCount', postCount);
403
+ */
404
+ selectSubquery<TValue = unknown, K extends string = string, TSub extends TableDef = TableDef>(
405
+ alias: K,
406
+ sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
407
+ ): SelectQueryBuilder<T & Record<K, TValue>, TTable> {
408
+ const query = resolveSelectQuery(sub);
409
+ return this.clone<T & Record<K, TValue>>(
410
+ this.projectionFacet.selectSubquery(this.context, alias, query)
411
+ );
412
+ }
413
+
414
+ /**
415
+ * Adds a JOIN against a derived table (subquery with alias)
416
+ * @param subquery - Subquery to join
417
+ * @param alias - Alias for the derived table
418
+ * @param condition - Join condition expression
419
+ * @param joinKind - Join kind (defaults to INNER)
420
+ * @param columnAliases - Optional column alias list for the derived table
421
+ * @returns New query builder instance with the derived-table join
422
+ * @example
423
+ * const activeUsers = new SelectQueryBuilder(userTable)
424
+ * .where(eq(userTable.columns.active, true));
425
+ * qb.joinSubquery(
426
+ * activeUsers,
427
+ * 'au',
428
+ * eq(col('t.userId'), col('au.id')),
429
+ * JOIN_KINDS.LEFT
430
+ * );
431
+ */
432
+ joinSubquery<TSub extends TableDef>(
433
+ subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
434
+ alias: string,
435
+ condition: BinaryExpressionNode,
436
+ joinKind: JoinKind = JOIN_KINDS.INNER,
437
+ columnAliases?: string[]
438
+ ): SelectQueryBuilder<T, TTable> {
439
+ const subAst = resolveSelectQuery(subquery);
440
+ const nextContext = this.joinFacet.joinSubquery(this.context, subAst, alias, condition, joinKind, columnAliases);
441
+ return this.clone(nextContext);
442
+ }
443
+
444
+ /**
445
+ * Adds a join against a function table (e.g., `generate_series`) using `fnTable` internally.
446
+ * @param name - Function name
447
+ * @param args - Optional arguments passed to the function
448
+ * @param alias - Alias for the function table so columns can be referenced
449
+ * @param condition - Join condition expression
450
+ * @param joinKind - Kind of join (defaults to INNER)
451
+ * @param options - Optional metadata (lateral, ordinality, column aliases, schema)
452
+ * @example
453
+ * qb.joinFunctionTable(
454
+ * 'generate_series',
455
+ * [literal(1), literal(10)],
456
+ * 'gs',
457
+ * eq(col('t.value'), col('gs.value')),
458
+ * JOIN_KINDS.INNER,
459
+ * { columnAliases: ['value'] }
460
+ * );
461
+ */
462
+ joinFunctionTable(
463
+ name: string,
464
+ args: OperandNode[] = [],
465
+ alias: string,
466
+ condition: BinaryExpressionNode,
467
+ joinKind: JoinKind = JOIN_KINDS.INNER,
468
+ options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
469
+ ): SelectQueryBuilder<T, TTable> {
470
+ const nextContext = this.joinFacet.joinFunctionTable(this.context, name, args, alias, condition, joinKind, options);
471
+ return this.clone(nextContext);
472
+ }
473
+
474
+ /**
475
+ * Adds an INNER JOIN to the query
476
+ * @param table - Table to join
477
+ * @param condition - Join condition expression
478
+ * @returns New query builder instance with the INNER JOIN
479
+ * @example
480
+ * qb.innerJoin(
481
+ * postTable,
482
+ * eq(userTable.columns.id, postTable.columns.userId)
483
+ * );
484
+ */
485
+ innerJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
486
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
487
+ return this.clone(nextContext);
488
+ }
489
+
490
+ /**
491
+ * Adds a LEFT JOIN to the query
492
+ * @param table - Table to join
493
+ * @param condition - Join condition expression
494
+ * @returns New query builder instance with the LEFT JOIN
495
+ * @example
496
+ * qb.leftJoin(
497
+ * postTable,
498
+ * eq(userTable.columns.id, postTable.columns.userId)
499
+ * );
500
+ */
501
+ leftJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
502
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
503
+ return this.clone(nextContext);
504
+ }
505
+
506
+ /**
507
+ * Adds a RIGHT JOIN to the query
508
+ * @param table - Table to join
509
+ * @param condition - Join condition expression
510
+ * @returns New query builder instance with the RIGHT JOIN
511
+ * @example
512
+ * qb.rightJoin(
513
+ * postTable,
514
+ * eq(userTable.columns.id, postTable.columns.userId)
515
+ * );
516
+ */
517
+ rightJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
518
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
519
+ return this.clone(nextContext);
520
+ }
521
+
522
+ /**
523
+ * Matches records based on a relationship
524
+ * @param relationName - Name of the relationship to match
525
+ * @param predicate - Optional predicate expression
526
+ * @returns New query builder instance with the relationship match
527
+ * @example
528
+ * qb.match('posts', eq(postTable.columns.published, true));
529
+ */
530
+ match<K extends keyof TTable['relations'] & string>(
531
+ relationName: K,
532
+ predicate?: ExpressionNode
533
+ ): SelectQueryBuilder<T, TTable> {
534
+ const nextContext = this.relationFacet.match(this.context, relationName, predicate);
535
+ return this.clone(nextContext);
536
+ }
537
+
538
+ /**
539
+ * Joins a related table
540
+ * @param relationName - Name of the relationship to join
541
+ * @param joinKind - Type of join (defaults to INNER)
542
+ * @param extraCondition - Optional additional join condition
543
+ * @returns New query builder instance with the relationship join
544
+ * @example
545
+ * qb.joinRelation('posts', JOIN_KINDS.LEFT);
546
+ * @example
547
+ * qb.joinRelation('posts', JOIN_KINDS.INNER, eq(postTable.columns.published, true));
548
+ */
549
+ joinRelation<K extends keyof TTable['relations'] & string>(
550
+ relationName: K,
551
+ joinKind: JoinKind = JOIN_KINDS.INNER,
552
+ extraCondition?: ExpressionNode
553
+ ): SelectQueryBuilder<T, TTable> {
554
+ const nextContext = this.relationFacet.joinRelation(this.context, relationName, joinKind, extraCondition);
555
+ return this.clone(nextContext);
556
+ }
557
+
558
+ /**
559
+ * Includes related data in the query results
560
+ * @param relationName - Name of the relationship to include
561
+ * @param options - Optional include options
562
+ * @returns New query builder instance with the relationship inclusion
563
+ * @example
564
+ * qb.include('posts');
565
+ * @example
566
+ * qb.include('posts', { columns: ['id', 'title', 'published'] });
567
+ * @example
568
+ * qb.include('posts', {
569
+ * columns: ['id', 'title'],
570
+ * where: eq(postTable.columns.published, true)
571
+ * });
572
+ * @example
573
+ * qb.include({ posts: { include: { author: true } } });
574
+ */
575
+ include<K extends keyof TTable['relations'] & string>(
576
+ relationName: K,
577
+ options?: RelationIncludeNodeInput<TTable['relations'][K]>
578
+ ): SelectQueryBuilder<T, TTable>;
579
+ include(relations: RelationIncludeInput<TTable>): SelectQueryBuilder<T, TTable>;
580
+ include<K extends keyof TTable['relations'] & string>(
581
+ relationNameOrRelations: K | RelationIncludeInput<TTable>,
582
+ options?: RelationIncludeNodeInput<TTable['relations'][K]>
583
+ ): SelectQueryBuilder<T, TTable> {
584
+ if (typeof relationNameOrRelations === 'object' && relationNameOrRelations !== null) {
585
+ const normalized = normalizeRelationInclude(relationNameOrRelations as RelationIncludeInput<TableDef>);
586
+ let nextContext = this.context;
587
+ for (const [relationName, node] of Object.entries(normalized)) {
588
+ nextContext = this.relationFacet.include(nextContext, relationName, node.options);
589
+ }
590
+ const nextTree = mergeRelationIncludeTrees(this.includeTree, normalized);
591
+ return this.clone(nextContext, undefined, undefined, nextTree);
592
+ }
593
+
594
+ const relationName = relationNameOrRelations as string;
595
+ const normalizedNode = normalizeRelationIncludeNode(options);
596
+ const nextContext = this.relationFacet.include(this.context, relationName, normalizedNode.options);
597
+ const shouldStore = Boolean(normalizedNode.include || normalizedNode.options);
598
+ const nextTree = shouldStore
599
+ ? mergeRelationIncludeTrees(this.includeTree, { [relationName]: normalizedNode })
600
+ : this.includeTree;
601
+ return this.clone(nextContext, undefined, undefined, nextTree);
602
+ }
603
+
604
+ /**
605
+ * Includes a relation lazily in the query results
606
+ * @param relationName - Name of the relation to include lazily
607
+ * @param options - Optional include options for lazy loading
608
+ * @returns New query builder instance with lazy relation inclusion
609
+ * @example
610
+ * const qb = new SelectQueryBuilder(userTable).includeLazy('posts');
611
+ * const users = await qb.execute(session);
612
+ * // Access posts later - they will be loaded on demand
613
+ * const posts = await users[0].posts;
614
+ */
615
+ includeLazy<K extends keyof RelationMap<TTable>>(
616
+ relationName: K,
617
+ options?: TypedRelationIncludeOptions<TTable['relations'][K]>
618
+ ): SelectQueryBuilder<T, TTable> {
619
+ let nextContext = this.context;
620
+ const relation = this.env.table.relations[relationName as string];
621
+ if (relation?.type === RelationKinds.BelongsTo) {
622
+ const foreignKey = relation.foreignKey;
623
+ const fkColumn = this.env.table.columns[foreignKey];
624
+ if (fkColumn) {
625
+ const hasAlias = nextContext.state.ast.columns.some(col => {
626
+ const node = col as { alias?: string; name?: string };
627
+ return (node.alias ?? node.name) === foreignKey;
628
+ });
629
+ if (!hasAlias) {
630
+ nextContext = this.columnSelector.select(nextContext, { [foreignKey]: fkColumn });
631
+ }
632
+ }
633
+ }
634
+ const nextLazy = new Set(this.lazyRelations);
635
+ nextLazy.add(relationName as string);
636
+ const nextOptions = new Map(this.lazyRelationOptions);
637
+ if (options) {
638
+ nextOptions.set(relationName as string, options);
639
+ } else {
640
+ nextOptions.delete(relationName as string);
641
+ }
642
+ return this.clone(nextContext, nextLazy, nextOptions);
643
+ }
644
+
645
+ /**
646
+ * Convenience alias for including only specific columns from a relation.
647
+ * @example
648
+ * qb.includePick('posts', ['id', 'title', 'createdAt']);
649
+ */
650
+ includePick<
651
+ K extends keyof TTable['relations'] & string,
652
+ C extends RelationTargetColumns<TTable['relations'][K]>
653
+ >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
654
+ const options = { columns: cols as readonly C[] } as unknown as RelationIncludeNodeInput<TTable['relations'][K]>;
655
+ return this.include(relationName, options);
656
+ }
657
+
658
+ /**
659
+ * Selects columns for the root table and relations from an array of entries
660
+ * @param config - Configuration array for deep column selection
661
+ * @returns New query builder instance with deep column selections
662
+ * @example
663
+ * qb.selectColumnsDeep([
664
+ * { type: 'root', columns: ['id', 'name'] },
665
+ * { type: 'relation', relationName: 'posts', columns: ['id', 'title'] }
666
+ * ]);
667
+ */
668
+ selectColumnsDeep(config: DeepSelectConfig<TTable>): SelectQueryBuilder<T, TTable> {
669
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
670
+ let currBuilder: SelectQueryBuilder<T, TTable> = this;
671
+
672
+ for (const entry of config) {
673
+ if (entry.type === 'root') {
674
+ currBuilder = currBuilder.select(...entry.columns);
675
+ } else {
676
+ const options = { columns: entry.columns } as unknown as RelationIncludeNodeInput<TTable['relations'][typeof entry.relationName]>;
677
+ currBuilder = currBuilder.include(entry.relationName, options);
678
+ }
679
+ }
680
+
681
+ return currBuilder;
682
+ }
683
+
684
+ /**
685
+ * Gets the list of lazy relations
686
+ * @returns Array of lazy relation names
687
+ */
688
+ getLazyRelations(): (keyof RelationMap<TTable>)[] {
689
+ return Array.from(this.lazyRelations) as (keyof RelationMap<TTable>)[];
690
+ }
691
+
692
+ /**
693
+ * Gets lazy relation include options
694
+ * @returns Map of relation names to include options
695
+ */
696
+ getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
697
+ return new Map(this.lazyRelationOptions);
698
+ }
699
+
700
+ /**
701
+ * Gets normalized nested include information for runtime preloading.
702
+ */
703
+ getIncludeTree(): NormalizedRelationIncludeTree {
704
+ return cloneRelationIncludeTree(this.includeTree);
705
+ }
706
+
707
+ /**
708
+ * Gets the table definition for this query builder
709
+ * @returns Table definition
710
+ */
711
+ getTable(): TTable {
712
+ return this.env.table as TTable;
713
+ }
714
+
715
+ /**
716
+ * Ensures that if no columns are selected, all columns from the table are selected by default.
717
+ */
718
+ private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
719
+ const columns = this.context.state.ast.columns;
720
+ if (!columns || columns.length === 0) {
721
+ const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
722
+ return this.select(...columnKeys);
723
+ }
724
+ return this;
725
+ }
726
+
727
+ /**
728
+ * Executes the query and returns hydrated results.
729
+ * If the builder was created with an entity constructor (e.g. via selectFromEntity),
730
+ * this will automatically return fully materialized entity instances.
731
+ *
732
+ * @param ctx - ORM session context
733
+ * @returns Promise of entity instances (or objects if generic T is not an entity)
734
+ * @example
735
+ * const users = await selectFromEntity(User).execute(session);
736
+ * // users is User[]
737
+ * users[0] instanceof User; // true
738
+ */
739
+ async execute(ctx: OrmSession): Promise<T[]> {
740
+ if (this.entityConstructor) {
741
+ return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
742
+ }
743
+ const builder = this.ensureDefaultSelection();
744
+ return executeHydrated(ctx, builder) as unknown as T[];
745
+ }
746
+
747
+ /**
748
+ * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
749
+ * Use this if you want raw data even when using selectFromEntity.
750
+ *
751
+ * @param ctx - ORM session context
752
+ * @returns Promise of plain entity instances
753
+ * @example
754
+ * const rows = await selectFromEntity(User).executePlain(session);
755
+ * // rows is EntityInstance<UserTable>[] (plain objects)
756
+ * rows[0] instanceof User; // false
757
+ */
758
+ async executePlain(ctx: OrmSession): Promise<T[]> {
759
+ const builder = this.ensureDefaultSelection();
760
+ const rows = await executeHydratedPlain(ctx, builder);
761
+ return rows as T[];
762
+ }
763
+
764
+ /**
765
+ * Executes the query and returns results as real class instances.
766
+ * Unlike execute(), this returns actual instances of the decorated entity class
767
+ * with working methods and proper instanceof checks.
768
+ * @param entityClass - The entity class constructor
769
+ * @param ctx - ORM session context
770
+ * @returns Promise of entity class instances
771
+ * @example
772
+ * const users = await selectFromEntity(User)
773
+ * .include('posts')
774
+ * .executeAs(User, session);
775
+ * users[0] instanceof User; // true!
776
+ * users[0].getFullName(); // works!
777
+ */
778
+ async executeAs<TEntity extends object>(
779
+ entityClass: EntityConstructor<TEntity>,
780
+ ctx: OrmSession
781
+ ): Promise<TEntity[]> {
782
+ const builder = this.ensureDefaultSelection();
783
+ const results = await executeHydrated(ctx, builder);
784
+ return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
785
+ }
786
+
787
+ /**
788
+ * Executes a count query for the current builder without LIMIT/OFFSET clauses.
789
+ *
790
+ * @example
791
+ * const total = await qb.count(session);
792
+ */
793
+ async count(session: OrmSession): Promise<number> {
794
+ return executeCount(this.context, this.env, session);
795
+ }
796
+
797
+ /**
798
+ * Executes the query and returns both the paged items and the total.
799
+ *
800
+ * @example
801
+ * const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
802
+ */
803
+ async executePaged(
804
+ session: OrmSession,
805
+ options: { page: number; pageSize: number }
806
+ ): Promise<PaginatedResult<T>> {
807
+ const builder = this.ensureDefaultSelection();
808
+ return executePagedQuery(builder, session, options, sess => builder.count(sess));
809
+ }
810
+
811
+ /**
812
+ * Executes the query and returns an array of values for a single column.
813
+ * This is a convenience method to avoid manual `.map(r => r.column)`.
814
+ *
815
+ * @param column - The column name to extract
816
+ * @param ctx - ORM session context
817
+ * @returns Promise of an array containing only the values of the specified column
818
+ * @example
819
+ * const acronyms = await selectFromEntity(Organization)
820
+ * .select('acronym')
821
+ * .distinct(O.acronym)
822
+ * .pluck('acronym', session);
823
+ * // ['NASA', 'UN', 'WHO']
824
+ */
825
+ async pluck<K extends keyof T & string>(
826
+ column: K,
827
+ ctx: OrmSession
828
+ ): Promise<T[K][]> {
829
+ const rows = await this.executePlain(ctx);
830
+ return rows.map(r => (r as Record<string, unknown>)[column]) as T[K][];
831
+ }
832
+
833
+ /**
834
+ * Executes the query with provided execution and hydration contexts
835
+ * @param execCtx - Execution context
836
+ * @param hydCtx - Hydration context
837
+ * @returns Promise of entity instances
838
+ * @example
839
+ * const execCtx = new ExecutionContext(session);
840
+ * const hydCtx = new HydrationContext();
841
+ * const users = await qb.executeWithContexts(execCtx, hydCtx);
842
+ */
843
+ async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
844
+ const builder = this.ensureDefaultSelection();
845
+ const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
846
+ if (this.entityConstructor) {
847
+ return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
848
+ }
849
+ return results as unknown as T[];
850
+ }
851
+
852
+ /**
853
+ * Adds a WHERE condition to the query
854
+ * @param expr - Expression for the WHERE clause
855
+ * @returns New query builder instance with the WHERE condition
856
+ * @example
857
+ * qb.where(eq(userTable.columns.id, 1));
858
+ * @example
859
+ * qb.where(and(
860
+ * eq(userTable.columns.active, true),
861
+ * gt(userTable.columns.createdAt, subDays(now(), 30))
862
+ * ));
863
+ */
864
+ where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
865
+ const nextContext = this.predicateFacet.where(this.context, expr);
866
+ return this.clone(nextContext);
867
+ }
868
+
869
+ /**
870
+ * Adds a GROUP BY clause to the query
871
+ * @param term - Column definition or ordering term to group by
872
+ * @returns New query builder instance with the GROUP BY clause
873
+ * @example
874
+ * qb.select('departmentId', count(userTable.columns.id))
875
+ * .groupBy(userTable.columns.departmentId);
876
+ */
877
+ groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
878
+ const nextContext = this.predicateFacet.groupBy(this.context, term);
879
+ return this.clone(nextContext);
880
+ }
881
+
882
+ /**
883
+ * Adds a HAVING condition to the query
884
+ * @param expr - Expression for the HAVING clause
885
+ * @returns New query builder instance with the HAVING condition
886
+ * @example
887
+ * qb.select('departmentId', count(userTable.columns.id))
888
+ * .groupBy(userTable.columns.departmentId)
889
+ * .having(gt(count(userTable.columns.id), 5));
890
+ */
891
+ having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
892
+ const nextContext = this.predicateFacet.having(this.context, expr);
893
+ return this.clone(nextContext);
894
+ }
895
+
896
+ /**
897
+ * Adds an ORDER BY clause to the query
898
+ * @param term - Column definition or ordering term to order by
899
+ * @param directionOrOptions - Order direction or options (defaults to ASC)
900
+ * @returns New query builder instance with the ORDER BY clause
901
+ *
902
+ * @example
903
+ * qb.orderBy(userTable.columns.createdAt, 'DESC');
904
+ */
905
+ orderBy(
906
+ term: ColumnDef | OrderingTerm,
907
+ directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string } = ORDER_DIRECTIONS.ASC
908
+ ): SelectQueryBuilder<T, TTable> {
909
+ const nextContext = applyOrderBy(this.context, this.predicateFacet, term, directionOrOptions);
910
+
911
+ return this.clone(nextContext);
912
+ }
913
+
914
+ /**
915
+ * Adds a DISTINCT clause to the query
916
+ * @param cols - Columns to make distinct
917
+ * @returns New query builder instance with the DISTINCT clause
918
+ * @example
919
+ * qb.distinct(userTable.columns.email);
920
+ * @example
921
+ * qb.distinct(userTable.columns.firstName, userTable.columns.lastName);
922
+ */
923
+ distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
924
+ return this.clone(this.projectionFacet.distinct(this.context, cols));
925
+ }
926
+
927
+ /**
928
+ * Adds a LIMIT clause to the query
929
+ * @param n - Maximum number of rows to return
930
+ * @returns New query builder instance with the LIMIT clause
931
+ * @example
932
+ * qb.limit(10);
933
+ * @example
934
+ * qb.limit(20).offset(40); // Pagination: page 3 with 20 items per page
935
+ */
936
+ limit(n: number): SelectQueryBuilder<T, TTable> {
937
+ const nextContext = this.predicateFacet.limit(this.context, n);
938
+ return this.clone(nextContext);
939
+ }
940
+
941
+ /**
942
+ * Adds an OFFSET clause to the query
943
+ * @param n - Number of rows to skip
944
+ * @returns New query builder instance with the OFFSET clause
945
+ * @example
946
+ * qb.offset(10);
947
+ * @example
948
+ * qb.limit(20).offset(40); // Pagination: page 3 with 20 items per page
949
+ */
950
+ offset(n: number): SelectQueryBuilder<T, TTable> {
951
+ const nextContext = this.predicateFacet.offset(this.context, n);
952
+ return this.clone(nextContext);
953
+ }
954
+
955
+ /**
956
+ * Combines this query with another using UNION
957
+ * @param query - Query to union with
958
+ * @returns New query builder instance with the set operation
959
+ * @example
960
+ * const activeUsers = new SelectQueryBuilder(userTable)
961
+ * .where(eq(userTable.columns.active, true));
962
+ * const inactiveUsers = new SelectQueryBuilder(userTable)
963
+ * .where(eq(userTable.columns.active, false));
964
+ * qb.union(activeUsers).union(inactiveUsers);
965
+ */
966
+ union<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
967
+ return this.clone(this.applySetOperation('UNION', query));
968
+ }
969
+
970
+ /**
971
+ * Combines this query with another using UNION ALL
972
+ * @param query - Query to union with
973
+ * @returns New query builder instance with the set operation
974
+ * @example
975
+ * const q1 = new SelectQueryBuilder(userTable).where(gt(userTable.columns.score, 80));
976
+ * const q2 = new SelectQueryBuilder(userTable).where(lt(userTable.columns.score, 20));
977
+ * qb.unionAll(q1).unionAll(q2);
978
+ */
979
+ unionAll<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
980
+ return this.clone(this.applySetOperation('UNION ALL', query));
981
+ }
982
+
983
+ /**
984
+ * Combines this query with another using INTERSECT
985
+ * @param query - Query to intersect with
986
+ * @returns New query builder instance with the set operation
987
+ * @example
988
+ * const activeUsers = new SelectQueryBuilder(userTable)
989
+ * .where(eq(userTable.columns.active, true));
990
+ * const premiumUsers = new SelectQueryBuilder(userTable)
991
+ * .where(eq(userTable.columns.premium, true));
992
+ * qb.intersect(activeUsers).intersect(premiumUsers);
993
+ */
994
+ intersect<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
995
+ return this.clone(this.applySetOperation('INTERSECT', query));
996
+ }
997
+
998
+ /**
999
+ * Combines this query with another using EXCEPT
1000
+ * @param query - Query to subtract
1001
+ * @returns New query builder instance with the set operation
1002
+ * @example
1003
+ * const allUsers = new SelectQueryBuilder(userTable);
1004
+ * const inactiveUsers = new SelectQueryBuilder(userTable)
1005
+ * .where(eq(userTable.columns.active, false));
1006
+ * qb.except(allUsers).except(inactiveUsers); // Only active users
1007
+ */
1008
+ except<TSub extends TableDef>(query: SelectQueryBuilder<T, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
1009
+ return this.clone(this.applySetOperation('EXCEPT', query));
1010
+ }
1011
+
1012
+ /**
1013
+ * Adds a WHERE EXISTS condition to the query
1014
+ * @param subquery - Subquery to check for existence
1015
+ * @returns New query builder instance with the WHERE EXISTS condition
1016
+ * @example
1017
+ * const postsQuery = new SelectQueryBuilder(postTable)
1018
+ * .where(eq(postTable.columns.userId, col('u.id')));
1019
+ * qb.whereExists(postsQuery);
1020
+ */
1021
+ whereExists<TSub extends TableDef>(
1022
+ subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
1023
+ correlate?: ExpressionNode
1024
+ ): SelectQueryBuilder<T, TTable> {
1025
+ const subAst = resolveSelectQuery(subquery);
1026
+ const correlated = this.applyCorrelation(subAst, correlate);
1027
+ return this.where(exists(correlated));
1028
+ }
1029
+
1030
+ /**
1031
+ * Adds a WHERE NOT EXISTS condition to the query
1032
+ * @param subquery - Subquery to check for non-existence
1033
+ * @returns New query builder instance with the WHERE NOT EXISTS condition
1034
+ * @example
1035
+ * const postsQuery = new SelectQueryBuilder(postTable)
1036
+ * .where(eq(postTable.columns.userId, col('u.id')));
1037
+ * qb.whereNotExists(postsQuery); // Users without posts
1038
+ */
1039
+ whereNotExists<TSub extends TableDef>(
1040
+ subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
1041
+ correlate?: ExpressionNode
1042
+ ): SelectQueryBuilder<T, TTable> {
1043
+ const subAst = resolveSelectQuery(subquery);
1044
+ const correlated = this.applyCorrelation(subAst, correlate);
1045
+ return this.where(notExists(correlated));
1046
+ }
1047
+
1048
+ /**
1049
+ * Adds a WHERE EXISTS condition based on a relationship
1050
+ * @param relationName - Name of the relationship to check
1051
+ * @param callback - Optional callback to modify the relationship query
1052
+ * @returns New query builder instance with the relationship existence check
1053
+ *
1054
+ * @example
1055
+ * qb.whereHas('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
1056
+ */
1057
+ whereHas<K extends keyof TTable['relations'] & string>(
1058
+ relationName: K,
1059
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
1060
+ maybeOptions?: WhereHasOptions
1061
+ ): SelectQueryBuilder<T, TTable> {
1062
+ const predicate = buildWhereHasPredicate(
1063
+ this.env,
1064
+ this.context,
1065
+ this.relationFacet,
1066
+ table => this.createChildBuilder(table),
1067
+ relationName,
1068
+ callbackOrOptions,
1069
+ maybeOptions,
1070
+ false
1071
+ );
1072
+
1073
+ return this.where(predicate);
1074
+ }
1075
+
1076
+ /**
1077
+ * Adds a WHERE NOT EXISTS condition based on a relationship
1078
+ * @param relationName - Name of the relationship to check
1079
+ * @param callback - Optional callback to modify the relationship query
1080
+ * @returns New query builder instance with the relationship non-existence check
1081
+ *
1082
+ * @example
1083
+ * qb.whereHasNot('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
1084
+ */
1085
+ whereHasNot<K extends keyof TTable['relations'] & string>(
1086
+ relationName: K,
1087
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
1088
+ maybeOptions?: WhereHasOptions
1089
+ ): SelectQueryBuilder<T, TTable> {
1090
+ const predicate = buildWhereHasPredicate(
1091
+ this.env,
1092
+ this.context,
1093
+ this.relationFacet,
1094
+ table => this.createChildBuilder(table),
1095
+ relationName,
1096
+ callbackOrOptions,
1097
+ maybeOptions,
1098
+ true
1099
+ );
1100
+
1101
+ return this.where(predicate);
1102
+ }
1103
+
1104
+ /**
1105
+ * Compiles the query to SQL for a specific dialect
1106
+ * @param dialect - Database dialect to compile for
1107
+ * @returns Compiled query with SQL and parameters
1108
+ * @example
1109
+ * const compiled = qb.select('id', 'name')
1110
+ * .where(eq(userTable.columns.active, true))
1111
+ * .compile('postgres');
1112
+ * console.log(compiled.sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
1113
+ */
1114
+ compile(dialect: SelectDialectInput): CompiledQuery {
1115
+ const resolved = resolveDialectInput(dialect);
1116
+ return resolved.compileSelect(this.getAST());
1117
+ }
1118
+
1119
+ /**
1120
+ * Converts the query to SQL string for a specific dialect
1121
+ * @param dialect - Database dialect to generate SQL for
1122
+ * @returns SQL string representation of the query
1123
+ * @example
1124
+ * const sql = qb.select('id', 'name')
1125
+ * .where(eq(userTable.columns.active, true))
1126
+ * .toSql('postgres');
1127
+ * console.log(sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
1128
+ */
1129
+ toSql(dialect: SelectDialectInput): string {
1130
+ return this.compile(dialect).sql;
1131
+ }
1132
+
1133
+ /**
1134
+ * Gets the hydration plan for the query
1135
+ * @returns Hydration plan or undefined if none exists
1136
+ * @example
1137
+ * const plan = qb.include('posts').getHydrationPlan();
1138
+ * console.log(plan?.relations); // Information about included relations
1139
+ */
1140
+ getHydrationPlan(): HydrationPlan | undefined {
1141
+ return this.context.hydration.getPlan();
1142
+ }
1143
+
1144
+ /**
1145
+ * Gets the Abstract Syntax Tree (AST) representation of the query
1146
+ * @returns Query AST with hydration applied
1147
+ * @example
1148
+ * const ast = qb.select('id', 'name').getAST();
1149
+ * console.log(ast.columns); // Array of column nodes
1150
+ * console.log(ast.from); // From clause information
1151
+ */
1152
+ getAST(): SelectQueryNode {
1153
+ return this.context.hydration.applyToAst(this.context.state.ast);
1154
+ }
1155
+ }