metal-orm 1.1.3 → 1.1.5

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