metal-orm 1.0.57 → 1.0.59

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 (46) hide show
  1. package/README.md +23 -13
  2. package/dist/index.cjs +1750 -733
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +244 -157
  5. package/dist/index.d.ts +244 -157
  6. package/dist/index.js +1745 -733
  7. package/dist/index.js.map +1 -1
  8. package/package.json +69 -69
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +186 -113
  11. package/src/decorators/column-decorator.ts +8 -49
  12. package/src/decorators/decorator-metadata.ts +10 -46
  13. package/src/decorators/entity.ts +30 -40
  14. package/src/decorators/relations.ts +30 -56
  15. package/src/orm/entity-hydration.ts +72 -0
  16. package/src/orm/entity-meta.ts +18 -13
  17. package/src/orm/entity-metadata.ts +240 -238
  18. package/src/orm/entity-relation-cache.ts +39 -0
  19. package/src/orm/entity-relations.ts +207 -0
  20. package/src/orm/entity.ts +124 -343
  21. package/src/orm/execute.ts +87 -20
  22. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  23. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  24. package/src/orm/lazy-batch/has-many.ts +69 -0
  25. package/src/orm/lazy-batch/has-one.ts +68 -0
  26. package/src/orm/lazy-batch/shared.ts +125 -0
  27. package/src/orm/lazy-batch.ts +4 -309
  28. package/src/orm/relations/belongs-to.ts +2 -2
  29. package/src/orm/relations/has-many.ts +23 -9
  30. package/src/orm/relations/has-one.ts +2 -2
  31. package/src/orm/relations/many-to-many.ts +29 -14
  32. package/src/orm/save-graph-types.ts +2 -2
  33. package/src/orm/save-graph.ts +18 -18
  34. package/src/query-builder/relation-conditions.ts +80 -59
  35. package/src/query-builder/relation-cte-builder.ts +63 -0
  36. package/src/query-builder/relation-filter-utils.ts +159 -0
  37. package/src/query-builder/relation-include-strategies.ts +177 -0
  38. package/src/query-builder/relation-join-planner.ts +80 -0
  39. package/src/query-builder/relation-service.ts +103 -159
  40. package/src/query-builder/relation-types.ts +43 -12
  41. package/src/query-builder/select/projection-facet.ts +23 -23
  42. package/src/query-builder/select/select-operations.ts +145 -0
  43. package/src/query-builder/select.ts +373 -426
  44. package/src/schema/relation.ts +22 -18
  45. package/src/schema/table.ts +22 -9
  46. package/src/schema/types.ts +103 -84
@@ -2,21 +2,20 @@ import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column-types.js';
3
3
  import { OrderingTerm, SelectQueryNode, SetOperationKind } from '../core/ast/query.js';
4
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 { derivedTable, fnTable } from '../core/ast/builders.js';
18
- import { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
19
- import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.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 { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
18
+ import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
20
19
 
21
20
  type SelectDialectInput = Dialect | DialectKey;
22
21
 
@@ -28,42 +27,46 @@ import {
28
27
  SelectQueryBuilderDependencies,
29
28
  SelectQueryBuilderEnvironment
30
29
  } from './select-query-builder-deps.js';
31
- import { QueryAstService } from './query-ast-service.js';
32
- import { ColumnSelector } from './column-selector.js';
33
- import { RelationManager } from './relation-manager.js';
34
- import { RelationIncludeOptions } from './relation-types.js';
35
- import type { RelationDef } from '../schema/relation.js';
30
+ import { ColumnSelector } from './column-selector.js';
31
+ import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
32
+ import { RelationKinds } from '../schema/relation.js';
36
33
  import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
37
- import { EntityInstance, RelationMap, RelationTargetTable } from '../schema/types.js';
38
- import { OrmSession } from '../orm/orm-session.ts';
39
- import { ExecutionContext } from '../orm/execution-context.js';
40
- import { HydrationContext } from '../orm/hydration-context.js';
41
- import { executeHydrated, executeHydratedWithContexts } from '../orm/execute.js';
42
- import { createJoinNode } from '../core/ast/join-node.js';
43
- import { resolveSelectQuery } from './query-resolution.js';
34
+ import { EntityInstance, RelationMap } from '../schema/types.js';
35
+ import { OrmSession } from '../orm/orm-session.ts';
36
+ import { ExecutionContext } from '../orm/execution-context.js';
37
+ import { HydrationContext } from '../orm/hydration-context.js';
38
+ import { executeHydrated, executeHydratedWithContexts } from '../orm/execute.js';
39
+ import { resolveSelectQuery } from './query-resolution.js';
40
+ import {
41
+ applyOrderBy,
42
+ buildWhereHasPredicate,
43
+ executeCount,
44
+ executePagedQuery,
45
+ RelationCallback,
46
+ WhereHasOptions
47
+ } from './select/select-operations.js';
48
+ import { SelectFromFacet } from './select/from-facet.js';
49
+ import { SelectJoinFacet } from './select/join-facet.js';
50
+ import { SelectProjectionFacet } from './select/projection-facet.js';
51
+ import { SelectPredicateFacet } from './select/predicate-facet.js';
52
+ import { SelectCTEFacet } from './select/cte-facet.js';
53
+ import { SelectSetOpFacet } from './select/setop-facet.js';
54
+ import { SelectRelationFacet } from './select/relation-facet.js';
44
55
 
45
56
 
46
57
  type ColumnSelectionValue = ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode;
47
58
 
48
- type DeepSelectEntry<TTable extends TableDef> = {
49
- type: 'root';
50
- columns: (keyof TTable['columns'] & string)[];
51
- } | {
52
- type: 'relation';
53
- relationName: keyof TTable['relations'] & string;
54
- columns: string[];
55
- };
59
+ type DeepSelectEntry<TTable extends TableDef> = {
60
+ type: 'root';
61
+ columns: (keyof TTable['columns'] & string)[];
62
+ } | {
63
+ type: 'relation';
64
+ relationName: keyof TTable['relations'] & string;
65
+ columns: string[];
66
+ };
56
67
 
57
68
  type DeepSelectConfig<TTable extends TableDef> = DeepSelectEntry<TTable>[];
58
69
 
59
- type WhereHasOptions = {
60
- correlate?: ExpressionNode;
61
- };
62
-
63
- type RelationCallback = <TChildTable extends TableDef>(
64
- qb: SelectQueryBuilder<unknown, TChildTable>
65
- ) => SelectQueryBuilder<unknown, TChildTable>;
66
-
67
70
 
68
71
  /**
69
72
  * Main query builder class for constructing SQL SELECT queries
@@ -71,11 +74,18 @@ type RelationCallback = <TChildTable extends TableDef>(
71
74
  * @typeParam TTable - Table definition being queried
72
75
  */
73
76
  export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef> {
74
- private readonly env: SelectQueryBuilderEnvironment;
75
- private readonly context: SelectQueryBuilderContext;
76
- private readonly columnSelector: ColumnSelector;
77
- private readonly relationManager: RelationManager;
78
- private readonly lazyRelations: Set<string>;
77
+ private readonly env: SelectQueryBuilderEnvironment;
78
+ private readonly context: SelectQueryBuilderContext;
79
+ private readonly columnSelector: ColumnSelector;
80
+ private readonly fromFacet: SelectFromFacet;
81
+ private readonly joinFacet: SelectJoinFacet;
82
+ private readonly projectionFacet: SelectProjectionFacet;
83
+ private readonly predicateFacet: SelectPredicateFacet;
84
+ private readonly cteFacet: SelectCTEFacet;
85
+ private readonly setOpFacet: SelectSetOpFacet;
86
+ private readonly relationFacet: SelectRelationFacet;
87
+ private readonly lazyRelations: Set<string>;
88
+ private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
79
89
 
80
90
  /**
81
91
  * Creates a new SelectQueryBuilder instance
@@ -89,20 +99,30 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
89
99
  state?: SelectQueryState,
90
100
  hydration?: HydrationManager,
91
101
  dependencies?: Partial<SelectQueryBuilderDependencies>,
92
- lazyRelations?: Set<string>
93
- ) {
94
- const deps = resolveSelectQueryBuilderDependencies(dependencies);
95
- this.env = { table, deps };
96
- const initialState = state ?? deps.createState(table);
97
- const initialHydration = hydration ?? deps.createHydration(table);
98
- this.context = {
99
- state: initialState,
100
- hydration: initialHydration
101
- };
102
- this.lazyRelations = new Set(lazyRelations ?? []);
103
- this.columnSelector = deps.createColumnSelector(this.env);
104
- this.relationManager = deps.createRelationManager(this.env);
105
- }
102
+ lazyRelations?: Set<string>,
103
+ lazyRelationOptions?: Map<string, RelationIncludeOptions>
104
+ ) {
105
+ const deps = resolveSelectQueryBuilderDependencies(dependencies);
106
+ this.env = { table, deps };
107
+ const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
108
+ const initialState = state ?? deps.createState(table);
109
+ const initialHydration = hydration ?? deps.createHydration(table);
110
+ this.context = {
111
+ state: initialState,
112
+ hydration: initialHydration
113
+ };
114
+ this.lazyRelations = new Set(lazyRelations ?? []);
115
+ this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
116
+ this.columnSelector = deps.createColumnSelector(this.env);
117
+ const relationManager = deps.createRelationManager(this.env);
118
+ this.fromFacet = new SelectFromFacet(this.env, createAstService);
119
+ this.joinFacet = new SelectJoinFacet(this.env, createAstService);
120
+ this.projectionFacet = new SelectProjectionFacet(this.columnSelector);
121
+ this.predicateFacet = new SelectPredicateFacet(this.env, createAstService);
122
+ this.cteFacet = new SelectCTEFacet(this.env, createAstService);
123
+ this.setOpFacet = new SelectSetOpFacet(this.env, createAstService);
124
+ this.relationFacet = new SelectRelationFacet(relationManager);
125
+ }
106
126
 
107
127
  /**
108
128
  * Creates a new SelectQueryBuilder instance with updated context and lazy relations
@@ -112,24 +132,27 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
112
132
  */
113
133
  private clone(
114
134
  context: SelectQueryBuilderContext = this.context,
115
- lazyRelations = new Set(this.lazyRelations)
135
+ lazyRelations = new Set(this.lazyRelations),
136
+ lazyRelationOptions = new Map(this.lazyRelationOptions)
116
137
  ): SelectQueryBuilder<T, TTable> {
117
- return new SelectQueryBuilder(this.env.table as TTable, context.state, context.hydration, this.env.deps, lazyRelations);
138
+ return new SelectQueryBuilder(
139
+ this.env.table as TTable,
140
+ context.state,
141
+ context.hydration,
142
+ this.env.deps,
143
+ lazyRelations,
144
+ lazyRelationOptions
145
+ );
118
146
  }
119
147
 
120
148
  /**
121
149
  * Applies an alias to the root FROM table.
122
150
  * @param alias - Alias to apply
123
151
  */
124
- as(alias: string): SelectQueryBuilder<T, TTable> {
125
- const from = this.context.state.ast.from;
126
- if (from.type !== 'Table') {
127
- throw new Error('Cannot alias non-table FROM sources');
128
- }
129
- const nextFrom = { ...from, alias };
130
- const nextContext = this.applyAst(this.context, service => service.withFrom(nextFrom));
131
- return this.clone(nextContext);
132
- }
152
+ as(alias: string): SelectQueryBuilder<T, TTable> {
153
+ const nextContext = this.fromFacet.as(this.context, alias);
154
+ return this.clone(nextContext);
155
+ }
133
156
 
134
157
 
135
158
 
@@ -157,52 +180,19 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
157
180
  return new SelectQueryBuilder(table, undefined, undefined, this.env.deps);
158
181
  }
159
182
 
160
- /**
161
- * Applies an AST mutation using the query AST service
162
- * @param context - Current query context
163
- * @param mutator - Function that mutates the AST
164
- * @returns Updated query context
165
- */
166
- private applyAst(
167
- context: SelectQueryBuilderContext,
168
- mutator: (service: QueryAstService) => SelectQueryState
169
- ): SelectQueryBuilderContext {
170
- const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
171
- const nextState = mutator(astService);
172
- return { state: nextState, hydration: context.hydration };
173
- }
174
-
175
- /**
176
- * Applies a join to the query context
177
- * @param context - Current query context
178
- * @param table - Table to join
179
- * @param condition - Join condition
180
- * @param kind - Join kind
181
- * @returns Updated query context with join applied
182
- */
183
- private applyJoin(
184
- context: SelectQueryBuilderContext,
185
- table: TableDef,
186
- condition: BinaryExpressionNode,
187
- kind: JoinKind
188
- ): SelectQueryBuilderContext {
189
- const joinNode = createJoinNode(kind, { type: 'Table', name: table.name, schema: table.schema }, condition);
190
- return this.applyAst(context, service => service.withJoin(joinNode));
191
- }
192
-
193
- /**
194
- * Applies a set operation to the query
195
- * @param operator - Set operation kind
196
- * @param query - Query to combine with
197
- * @returns Updated query context with set operation
198
- */
199
- private applySetOperation<TSub extends TableDef>(
200
- operator: SetOperationKind,
201
- query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
202
- ): SelectQueryBuilderContext {
203
- const subAst = resolveSelectQuery(query);
204
- return this.applyAst(this.context, service => service.withSetOperation(operator, subAst));
205
- }
183
+ /**
184
+ * Applies a set operation to the query
185
+ * @param operator - Set operation kind
186
+ * @param query - Query to combine with
187
+ * @returns Updated query context with set operation
188
+ */
189
+ private applySetOperation<TSub extends TableDef>(
190
+ operator: SetOperationKind,
191
+ query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
192
+ ): SelectQueryBuilderContext {
193
+ const subAst = resolveSelectQuery(query);
194
+ return this.setOpFacet.applySetOperation(this.context, operator, subAst);
195
+ }
206
196
 
207
197
 
208
198
  /**
@@ -219,10 +209,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
219
209
  ...args: K[] | [Record<string, ColumnSelectionValue>]
220
210
  ): SelectQueryBuilder<T, TTable> {
221
211
  // If first arg is an object (not a string), treat as projection map
222
- if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
223
- const columns = args[0] as Record<string, ColumnSelectionValue>;
224
- return this.clone(this.columnSelector.select(this.context, columns));
225
- }
212
+ if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
213
+ const columns = args[0] as Record<string, ColumnSelectionValue>;
214
+ return this.clone(this.projectionFacet.select(this.context, columns));
215
+ }
226
216
 
227
217
  // Otherwise, treat as column names
228
218
  const cols = args as K[];
@@ -235,17 +225,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
235
225
  selection[key] = col;
236
226
  }
237
227
 
238
- return this.clone(this.columnSelector.select(this.context, selection));
239
- }
228
+ return this.clone(this.projectionFacet.select(this.context, selection));
229
+ }
240
230
 
241
231
  /**
242
232
  * Selects raw column expressions
243
233
  * @param cols - Column expressions as strings
244
234
  * @returns New query builder instance with raw column selections
245
235
  */
246
- selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
247
- return this.clone(this.columnSelector.selectRaw(this.context, cols));
248
- }
236
+ selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
237
+ return this.clone(this.projectionFacet.selectRaw(this.context, cols));
238
+ }
249
239
 
250
240
  /**
251
241
  * Adds a Common Table Expression (CTE) to the query
@@ -254,11 +244,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
254
244
  * @param columns - Optional column names for the CTE
255
245
  * @returns New query builder instance with the CTE
256
246
  */
257
- with<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
258
- const subAst = resolveSelectQuery(query);
259
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, false));
260
- return this.clone(nextContext);
261
- }
247
+ with<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
248
+ const subAst = resolveSelectQuery(query);
249
+ const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, false);
250
+ return this.clone(nextContext);
251
+ }
262
252
 
263
253
  /**
264
254
  * Adds a recursive Common Table Expression (CTE) to the query
@@ -267,11 +257,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
267
257
  * @param columns - Optional column names for the CTE
268
258
  * @returns New query builder instance with the recursive CTE
269
259
  */
270
- withRecursive<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
271
- const subAst = resolveSelectQuery(query);
272
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, true));
273
- return this.clone(nextContext);
274
- }
260
+ withRecursive<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
261
+ const subAst = resolveSelectQuery(query);
262
+ const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, true);
263
+ return this.clone(nextContext);
264
+ }
275
265
 
276
266
  /**
277
267
  * Replaces the FROM clause with a derived table (subquery with alias)
@@ -280,16 +270,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
280
270
  * @param columnAliases - Optional column alias list
281
271
  * @returns New query builder instance with updated FROM
282
272
  */
283
- fromSubquery<TSub extends TableDef>(
284
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
285
- alias: string,
286
- columnAliases?: string[]
287
- ): SelectQueryBuilder<T, TTable> {
288
- const subAst = resolveSelectQuery(subquery);
289
- const fromNode = derivedTable(subAst, alias, columnAliases);
290
- const nextContext = this.applyAst(this.context, service => service.withFrom(fromNode));
291
- return this.clone(nextContext);
292
- }
273
+ fromSubquery<TSub extends TableDef>(
274
+ subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
275
+ alias: string,
276
+ columnAliases?: string[]
277
+ ): SelectQueryBuilder<T, TTable> {
278
+ const subAst = resolveSelectQuery(subquery);
279
+ const nextContext = this.fromFacet.fromSubquery(this.context, subAst, alias, columnAliases);
280
+ return this.clone(nextContext);
281
+ }
293
282
 
294
283
  /**
295
284
  * Replaces the FROM clause with a function table expression.
@@ -298,16 +287,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
298
287
  * @param alias - Optional alias for the function table
299
288
  * @param options - Optional function-table metadata (lateral, ordinality, column aliases, schema)
300
289
  */
301
- fromFunctionTable(
302
- name: string,
303
- args: OperandNode[] = [],
304
- alias?: string,
305
- options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
306
- ): SelectQueryBuilder<T, TTable> {
307
- const functionTable = fnTable(name, args, alias, options);
308
- const nextContext = this.applyAst(this.context, service => service.withFrom(functionTable));
309
- return this.clone(nextContext);
310
- }
290
+ fromFunctionTable(
291
+ name: string,
292
+ args: OperandNode[] = [],
293
+ alias?: string,
294
+ options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
295
+ ): SelectQueryBuilder<T, TTable> {
296
+ const nextContext = this.fromFacet.fromFunctionTable(this.context, name, args, alias, options);
297
+ return this.clone(nextContext);
298
+ }
311
299
 
312
300
  /**
313
301
  * Selects a subquery as a column
@@ -315,10 +303,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
315
303
  * @param sub - Query builder or query node for the subquery
316
304
  * @returns New query builder instance with the subquery selection
317
305
  */
318
- selectSubquery<TSub extends TableDef>(alias: string, sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
319
- const query = resolveSelectQuery(sub);
320
- return this.clone(this.columnSelector.selectSubquery(this.context, alias, query));
321
- }
306
+ selectSubquery<TSub extends TableDef>(alias: string, sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
307
+ const query = resolveSelectQuery(sub);
308
+ return this.clone(this.projectionFacet.selectSubquery(this.context, alias, query));
309
+ }
322
310
 
323
311
  /**
324
312
  * Adds a JOIN against a derived table (subquery with alias)
@@ -329,18 +317,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
329
317
  * @param columnAliases - Optional column alias list for the derived table
330
318
  * @returns New query builder instance with the derived-table join
331
319
  */
332
- joinSubquery<TSub extends TableDef>(
333
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
334
- alias: string,
335
- condition: BinaryExpressionNode,
336
- joinKind: JoinKind = JOIN_KINDS.INNER,
337
- columnAliases?: string[]
338
- ): SelectQueryBuilder<T, TTable> {
339
- const subAst = resolveSelectQuery(subquery);
340
- const joinNode = createJoinNode(joinKind, derivedTable(subAst, alias, columnAliases), condition);
341
- const nextContext = this.applyAst(this.context, service => service.withJoin(joinNode));
342
- return this.clone(nextContext);
343
- }
320
+ joinSubquery<TSub extends TableDef>(
321
+ subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
322
+ alias: string,
323
+ condition: BinaryExpressionNode,
324
+ joinKind: JoinKind = JOIN_KINDS.INNER,
325
+ columnAliases?: string[]
326
+ ): SelectQueryBuilder<T, TTable> {
327
+ const subAst = resolveSelectQuery(subquery);
328
+ const nextContext = this.joinFacet.joinSubquery(this.context, subAst, alias, condition, joinKind, columnAliases);
329
+ return this.clone(nextContext);
330
+ }
344
331
 
345
332
  /**
346
333
  * Adds a join against a function table (e.g., `generate_series`) using `fnTable` internally.
@@ -351,19 +338,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
351
338
  * @param joinKind - Kind of join (defaults to INNER)
352
339
  * @param options - Optional metadata (lateral, ordinality, column aliases, schema)
353
340
  */
354
- joinFunctionTable(
355
- name: string,
356
- args: OperandNode[] = [],
357
- alias: string,
358
- condition: BinaryExpressionNode,
359
- joinKind: JoinKind = JOIN_KINDS.INNER,
360
- options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
361
- ): SelectQueryBuilder<T, TTable> {
362
- const functionTable = fnTable(name, args, alias, options);
363
- const joinNode = createJoinNode(joinKind, functionTable, condition);
364
- const nextContext = this.applyAst(this.context, service => service.withJoin(joinNode));
365
- return this.clone(nextContext);
366
- }
341
+ joinFunctionTable(
342
+ name: string,
343
+ args: OperandNode[] = [],
344
+ alias: string,
345
+ condition: BinaryExpressionNode,
346
+ joinKind: JoinKind = JOIN_KINDS.INNER,
347
+ options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
348
+ ): SelectQueryBuilder<T, TTable> {
349
+ const nextContext = this.joinFacet.joinFunctionTable(this.context, name, args, alias, condition, joinKind, options);
350
+ return this.clone(nextContext);
351
+ }
367
352
 
368
353
  /**
369
354
  * Adds an INNER JOIN to the query
@@ -371,10 +356,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
371
356
  * @param condition - Join condition expression
372
357
  * @returns New query builder instance with the INNER JOIN
373
358
  */
374
- innerJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
375
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
376
- return this.clone(nextContext);
377
- }
359
+ innerJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
360
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
361
+ return this.clone(nextContext);
362
+ }
378
363
 
379
364
  /**
380
365
  * Adds a LEFT JOIN to the query
@@ -382,10 +367,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
382
367
  * @param condition - Join condition expression
383
368
  * @returns New query builder instance with the LEFT JOIN
384
369
  */
385
- leftJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
386
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
387
- return this.clone(nextContext);
388
- }
370
+ leftJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
371
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
372
+ return this.clone(nextContext);
373
+ }
389
374
 
390
375
  /**
391
376
  * Adds a RIGHT JOIN to the query
@@ -393,10 +378,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
393
378
  * @param condition - Join condition expression
394
379
  * @returns New query builder instance with the RIGHT JOIN
395
380
  */
396
- rightJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
397
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
398
- return this.clone(nextContext);
399
- }
381
+ rightJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
382
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
383
+ return this.clone(nextContext);
384
+ }
400
385
 
401
386
  /**
402
387
  * Matches records based on a relationship
@@ -404,13 +389,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
404
389
  * @param predicate - Optional predicate expression
405
390
  * @returns New query builder instance with the relationship match
406
391
  */
407
- match<K extends keyof TTable['relations'] & string>(
408
- relationName: K,
409
- predicate?: ExpressionNode
410
- ): SelectQueryBuilder<T, TTable> {
411
- const nextContext = this.relationManager.match(this.context, relationName, predicate);
412
- return this.clone(nextContext);
413
- }
392
+ match<K extends keyof TTable['relations'] & string>(
393
+ relationName: K,
394
+ predicate?: ExpressionNode
395
+ ): SelectQueryBuilder<T, TTable> {
396
+ const nextContext = this.relationFacet.match(this.context, relationName, predicate);
397
+ return this.clone(nextContext);
398
+ }
414
399
 
415
400
  /**
416
401
  * Joins a related table
@@ -419,14 +404,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
419
404
  * @param extraCondition - Optional additional join condition
420
405
  * @returns New query builder instance with the relationship join
421
406
  */
422
- joinRelation<K extends keyof TTable['relations'] & string>(
423
- relationName: K,
424
- joinKind: JoinKind = JOIN_KINDS.INNER,
425
- extraCondition?: ExpressionNode
426
- ): SelectQueryBuilder<T, TTable> {
427
- const nextContext = this.relationManager.joinRelation(this.context, relationName, joinKind, extraCondition);
428
- return this.clone(nextContext);
429
- }
407
+ joinRelation<K extends keyof TTable['relations'] & string>(
408
+ relationName: K,
409
+ joinKind: JoinKind = JOIN_KINDS.INNER,
410
+ extraCondition?: ExpressionNode
411
+ ): SelectQueryBuilder<T, TTable> {
412
+ const nextContext = this.relationFacet.joinRelation(this.context, relationName, joinKind, extraCondition);
413
+ return this.clone(nextContext);
414
+ }
430
415
 
431
416
  /**
432
417
  * Includes related data in the query results
@@ -434,62 +419,60 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
434
419
  * @param options - Optional include options
435
420
  * @returns New query builder instance with the relationship inclusion
436
421
  */
437
- include<K extends keyof TTable['relations'] & string>(
438
- relationName: K,
439
- options?: RelationIncludeOptions
440
- ): SelectQueryBuilder<T, TTable> {
441
- const nextContext = this.relationManager.include(this.context, relationName, options);
442
- return this.clone(nextContext);
443
- }
422
+ include<K extends keyof TTable['relations'] & string>(
423
+ relationName: K,
424
+ options?: TypedRelationIncludeOptions<TTable['relations'][K]>
425
+ ): SelectQueryBuilder<T, TTable> {
426
+ const nextContext = this.relationFacet.include(this.context, relationName, options);
427
+ return this.clone(nextContext);
428
+ }
444
429
 
445
430
  /**
446
431
  * Includes a relation lazily in the query results
447
432
  * @param relationName - Name of the relation to include lazily
433
+ * @param options - Optional include options for lazy loading
448
434
  * @returns New query builder instance with lazy relation inclusion
449
435
  */
450
- includeLazy<K extends keyof RelationMap<TTable>>(relationName: K): SelectQueryBuilder<T, TTable> {
436
+ includeLazy<K extends keyof RelationMap<TTable>>(
437
+ relationName: K,
438
+ options?: TypedRelationIncludeOptions<TTable['relations'][K]>
439
+ ): SelectQueryBuilder<T, TTable> {
440
+ let nextContext = this.context;
441
+ const relation = this.env.table.relations[relationName as string];
442
+ if (relation?.type === RelationKinds.BelongsTo) {
443
+ const foreignKey = relation.foreignKey;
444
+ const fkColumn = this.env.table.columns[foreignKey];
445
+ if (fkColumn) {
446
+ const hasAlias = nextContext.state.ast.columns.some(col => {
447
+ const node = col as { alias?: string; name?: string };
448
+ return (node.alias ?? node.name) === foreignKey;
449
+ });
450
+ if (!hasAlias) {
451
+ nextContext = this.columnSelector.select(nextContext, { [foreignKey]: fkColumn });
452
+ }
453
+ }
454
+ }
451
455
  const nextLazy = new Set(this.lazyRelations);
452
456
  nextLazy.add(relationName as string);
453
- return this.clone(this.context, nextLazy);
454
- }
455
-
456
- /**
457
- * Selects columns for a related table in a single hop.
458
- */
459
- selectRelationColumns<
460
- K extends keyof TTable['relations'] & string,
461
- TRel extends RelationDef = TTable['relations'][K],
462
- TTarget extends TableDef = RelationTargetTable<TRel>,
463
- C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
464
- >(relationName: K, ...cols: C[]): SelectQueryBuilder<T, TTable> {
465
- const relation = this.env.table.relations[relationName] as RelationDef | undefined;
466
- if (!relation) {
467
- throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
457
+ const nextOptions = new Map(this.lazyRelationOptions);
458
+ if (options) {
459
+ nextOptions.set(relationName as string, options);
460
+ } else {
461
+ nextOptions.delete(relationName as string);
468
462
  }
469
- const target = relation.target;
470
-
471
- for (const col of cols) {
472
- if (!target.columns[col]) {
473
- throw new Error(
474
- `Column '${col}' not found on related table '${target.name}' for relation '${relationName}'`
475
- );
476
- }
477
- }
478
-
479
- return this.include(relationName as string, { columns: cols as string[] });
463
+ return this.clone(nextContext, nextLazy, nextOptions);
480
464
  }
481
465
 
482
466
  /**
483
- * Convenience alias for selecting specific columns from a relation.
467
+ * Convenience alias for including only specific columns from a relation.
484
468
  */
485
- includePick<
486
- K extends keyof TTable['relations'] & string,
487
- TRel extends RelationDef = TTable['relations'][K],
488
- TTarget extends TableDef = RelationTargetTable<TRel>,
489
- C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
490
- >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
491
- return this.selectRelationColumns(relationName, ...cols);
492
- }
469
+ includePick<
470
+ K extends keyof TTable['relations'] & string,
471
+ C extends RelationTargetColumns<TTable['relations'][K]>
472
+ >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
473
+ const options = { columns: cols as readonly C[] } as unknown as TypedRelationIncludeOptions<TTable['relations'][K]>;
474
+ return this.include(relationName, options);
475
+ }
493
476
 
494
477
 
495
478
  /**
@@ -501,13 +484,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
501
484
  // eslint-disable-next-line @typescript-eslint/no-this-alias
502
485
  let currBuilder: SelectQueryBuilder<T, TTable> = this;
503
486
 
504
- for (const entry of config) {
505
- if (entry.type === 'root') {
506
- currBuilder = currBuilder.select(...entry.columns);
507
- } else {
508
- currBuilder = currBuilder.selectRelationColumns(entry.relationName, ...(entry.columns as string[]));
509
- }
510
- }
487
+ for (const entry of config) {
488
+ if (entry.type === 'root') {
489
+ currBuilder = currBuilder.select(...entry.columns);
490
+ } else {
491
+ const options = { columns: entry.columns } as unknown as TypedRelationIncludeOptions<TTable['relations'][typeof entry.relationName]>;
492
+ currBuilder = currBuilder.include(entry.relationName, options);
493
+ }
494
+ }
511
495
 
512
496
  return currBuilder;
513
497
  }
@@ -520,6 +504,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
520
504
  return Array.from(this.lazyRelations) as (keyof RelationMap<TTable>)[];
521
505
  }
522
506
 
507
+ /**
508
+ * Gets lazy relation include options
509
+ * @returns Map of relation names to include options
510
+ */
511
+ getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
512
+ return new Map(this.lazyRelationOptions);
513
+ }
514
+
523
515
  /**
524
516
  * Gets the table definition for this query builder
525
517
  * @returns Table definition
@@ -537,63 +529,28 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
537
529
  return executeHydrated(ctx, this);
538
530
  }
539
531
 
540
- private withAst(ast: SelectQueryNode): SelectQueryBuilder<T, TTable> {
541
- const nextState = new SelectQueryState(this.env.table as TTable, ast);
542
- const nextContext: SelectQueryBuilderContext = {
543
- ...this.context,
544
- state: nextState
545
- };
546
- return this.clone(nextContext);
547
- }
548
-
549
- async count(session: OrmSession): Promise<number> {
550
- const unpagedAst: SelectQueryNode = {
551
- ...this.context.state.ast,
552
- orderBy: undefined,
553
- limit: undefined,
554
- offset: undefined
555
- };
556
-
557
- const subAst = this.withAst(unpagedAst).getAST();
558
-
559
- const countQuery: SelectQueryNode = {
560
- type: 'SelectQuery',
561
- from: derivedTable(subAst, '__metal_count'),
562
- columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
563
- joins: []
564
- };
565
-
566
- const execCtx = session.getExecutionContext();
567
- const compiled = execCtx.dialect.compileSelect(countQuery);
568
- const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
569
- const value = results[0]?.values?.[0]?.[0];
570
-
571
- if (typeof value === 'number') return value;
572
- if (typeof value === 'bigint') return Number(value);
573
- if (typeof value === 'string') return Number(value);
574
- return value === null || value === undefined ? 0 : Number(value);
575
- }
576
-
577
- async executePaged(
578
- session: OrmSession,
579
- options: { page: number; pageSize: number }
580
- ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
581
- const { page, pageSize } = options;
582
- if (!Number.isInteger(page) || page < 1) {
583
- throw new Error('executePaged: page must be an integer >= 1');
584
- }
585
- if (!Number.isInteger(pageSize) || pageSize < 1) {
586
- throw new Error('executePaged: pageSize must be an integer >= 1');
587
- }
588
-
589
- const offset = (page - 1) * pageSize;
590
- const [items, totalItems] = await Promise.all([
591
- this.limit(pageSize).offset(offset).execute(session),
592
- this.count(session)
593
- ]);
594
-
595
- return { items, totalItems };
596
- }
532
+ /**
533
+ * Executes a count query for the current builder without LIMIT/OFFSET clauses.
534
+ *
535
+ * @example
536
+ * const total = await qb.count(session);
537
+ */
538
+ async count(session: OrmSession): Promise<number> {
539
+ return executeCount(this.context, this.env, session);
540
+ }
541
+
542
+ /**
543
+ * Executes the query and returns both the paged items and the total.
544
+ *
545
+ * @example
546
+ * const { items, totalItems } = await qb.executePaged(session, { page: 1, pageSize: 20 });
547
+ */
548
+ async executePaged(
549
+ session: OrmSession,
550
+ options: { page: number; pageSize: number }
551
+ ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
552
+ return executePagedQuery(this, session, options, sess => this.count(sess));
553
+ }
597
554
 
598
555
  /**
599
556
  * Executes the query with provided execution and hydration contexts
@@ -610,81 +567,79 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
610
567
  * @param expr - Expression for the WHERE clause
611
568
  * @returns New query builder instance with the WHERE condition
612
569
  */
613
- where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
614
- const nextContext = this.applyAst(this.context, service => service.withWhere(expr));
615
- return this.clone(nextContext);
616
- }
570
+ where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
571
+ const nextContext = this.predicateFacet.where(this.context, expr);
572
+ return this.clone(nextContext);
573
+ }
617
574
 
618
575
  /**
619
576
  * Adds a GROUP BY clause to the query
620
577
  * @param term - Column definition or ordering term to group by
621
578
  * @returns New query builder instance with the GROUP BY clause
622
579
  */
623
- groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
624
- const nextContext = this.applyAst(this.context, service => service.withGroupBy(term));
625
- return this.clone(nextContext);
626
- }
580
+ groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
581
+ const nextContext = this.predicateFacet.groupBy(this.context, term);
582
+ return this.clone(nextContext);
583
+ }
627
584
 
628
585
  /**
629
586
  * Adds a HAVING condition to the query
630
587
  * @param expr - Expression for the HAVING clause
631
588
  * @returns New query builder instance with the HAVING condition
632
589
  */
633
- having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
634
- const nextContext = this.applyAst(this.context, service => service.withHaving(expr));
635
- return this.clone(nextContext);
636
- }
637
-
590
+ having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
591
+ const nextContext = this.predicateFacet.having(this.context, expr);
592
+ return this.clone(nextContext);
593
+ }
638
594
 
639
595
 
640
- /**
641
- * Adds an ORDER BY clause to the query
642
- * @param term - Column definition or ordering term to order by
643
- * @param directionOrOptions - Order direction or options (defaults to ASC)
644
- * @returns New query builder instance with the ORDER BY clause
645
- */
646
- orderBy(
647
- term: ColumnDef | OrderingTerm,
648
- directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string } = ORDER_DIRECTIONS.ASC
649
- ): SelectQueryBuilder<T, TTable> {
650
- const options = typeof directionOrOptions === 'string' ? { direction: directionOrOptions } : directionOrOptions;
651
- const dir = options.direction ?? ORDER_DIRECTIONS.ASC;
652
-
653
- const nextContext = this.applyAst(this.context, service =>
654
- service.withOrderBy(term, dir, options.nulls, options.collation)
655
- );
656
596
 
657
- return this.clone(nextContext);
658
- }
597
+ /**
598
+ * Adds an ORDER BY clause to the query
599
+ * @param term - Column definition or ordering term to order by
600
+ * @param directionOrOptions - Order direction or options (defaults to ASC)
601
+ * @returns New query builder instance with the ORDER BY clause
602
+ *
603
+ * @example
604
+ * qb.orderBy(userTable.columns.createdAt, 'DESC');
605
+ */
606
+ orderBy(
607
+ term: ColumnDef | OrderingTerm,
608
+ directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string } = ORDER_DIRECTIONS.ASC
609
+ ): SelectQueryBuilder<T, TTable> {
610
+ const nextContext = applyOrderBy(this.context, this.predicateFacet, term, directionOrOptions);
611
+
612
+ return this.clone(nextContext);
613
+ }
659
614
 
660
615
  /**
661
616
  * Adds a DISTINCT clause to the query
662
617
  * @param cols - Columns to make distinct
663
618
  * @returns New query builder instance with the DISTINCT clause
664
619
  */
665
- distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
666
- return this.clone(this.columnSelector.distinct(this.context, cols));
667
- }
620
+ distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
621
+ return this.clone(this.projectionFacet.distinct(this.context, cols));
622
+ }
668
623
 
669
624
  /**
670
625
  * Adds a LIMIT clause to the query
671
626
  * @param n - Maximum number of rows to return
672
627
  * @returns New query builder instance with the LIMIT clause
673
628
  */
674
- limit(n: number): SelectQueryBuilder<T, TTable> {
675
- const nextContext = this.applyAst(this.context, service => service.withLimit(n));
676
- return this.clone(nextContext);
677
- }
629
+ limit(n: number): SelectQueryBuilder<T, TTable> {
630
+ const nextContext = this.predicateFacet.limit(this.context, n);
631
+ return this.clone(nextContext);
632
+ }
678
633
 
679
634
  /**
680
635
  * Adds an OFFSET clause to the query
681
636
  * @param n - Number of rows to skip
682
637
  * @returns New query builder instance with the OFFSET clause
683
638
  */
684
- offset(n: number): SelectQueryBuilder<T, TTable> {
685
- const nextContext = this.applyAst(this.context, service => service.withOffset(n));
686
- return this.clone(nextContext);
687
- }
639
+ offset(n: number): SelectQueryBuilder<T, TTable> {
640
+ const nextContext = this.predicateFacet.offset(this.context, n);
641
+ return this.clone(nextContext);
642
+ }
688
643
 
689
644
  /**
690
645
  * Combines this query with another using UNION
@@ -750,69 +705,61 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
750
705
  return this.where(notExists(correlated));
751
706
  }
752
707
 
753
- /**
754
- * Adds a WHERE EXISTS condition based on a relationship
755
- * @param relationName - Name of the relationship to check
756
- * @param callback - Optional callback to modify the relationship query
757
- * @returns New query builder instance with the relationship existence check
758
- */
759
- whereHas<K extends keyof TTable['relations'] & string>(
760
- relationName: K,
761
- callbackOrOptions?: RelationCallback | WhereHasOptions,
762
- maybeOptions?: WhereHasOptions
763
- ): SelectQueryBuilder<T, TTable> {
764
- const relation = this.env.table.relations[relationName];
765
-
766
- if (!relation) {
767
- throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
768
- }
769
-
770
- const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions as RelationCallback : undefined;
771
- const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as WhereHasOptions | undefined;
772
-
773
- let subQb = this.createChildBuilder<unknown, typeof relation.target>(relation.target);
774
-
775
- if (callback) {
776
- subQb = callback(subQb);
777
- }
778
-
779
- const subAst = subQb.getAST();
780
- const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst, options?.correlate);
781
-
782
- return this.where(exists(finalSubAst));
783
- }
784
-
785
- /**
786
- * Adds a WHERE NOT EXISTS condition based on a relationship
787
- * @param relationName - Name of the relationship to check
788
- * @param callback - Optional callback to modify the relationship query
789
- * @returns New query builder instance with the relationship non-existence check
790
- */
791
- whereHasNot<K extends keyof TTable['relations'] & string>(
792
- relationName: K,
793
- callbackOrOptions?: RelationCallback | WhereHasOptions,
794
- maybeOptions?: WhereHasOptions
795
- ): SelectQueryBuilder<T, TTable> {
796
- const relation = this.env.table.relations[relationName];
797
-
798
- if (!relation) {
799
- throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
800
- }
801
-
802
- const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions as RelationCallback : undefined;
803
- const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as WhereHasOptions | undefined;
804
-
805
- let subQb = this.createChildBuilder<unknown, typeof relation.target>(relation.target);
806
-
807
- if (callback) {
808
- subQb = callback(subQb);
809
- }
810
-
811
- const subAst = subQb.getAST();
812
- const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst, options?.correlate);
813
-
814
- return this.where(notExists(finalSubAst));
815
- }
708
+ /**
709
+ * Adds a WHERE EXISTS condition based on a relationship
710
+ * @param relationName - Name of the relationship to check
711
+ * @param callback - Optional callback to modify the relationship query
712
+ * @returns New query builder instance with the relationship existence check
713
+ *
714
+ * @example
715
+ * qb.whereHas('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
716
+ */
717
+ whereHas<K extends keyof TTable['relations'] & string>(
718
+ relationName: K,
719
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
720
+ maybeOptions?: WhereHasOptions
721
+ ): SelectQueryBuilder<T, TTable> {
722
+ const predicate = buildWhereHasPredicate(
723
+ this.env,
724
+ this.context,
725
+ this.relationFacet,
726
+ table => this.createChildBuilder(table),
727
+ relationName,
728
+ callbackOrOptions,
729
+ maybeOptions,
730
+ false
731
+ );
732
+
733
+ return this.where(predicate);
734
+ }
735
+
736
+ /**
737
+ * Adds a WHERE NOT EXISTS condition based on a relationship
738
+ * @param relationName - Name of the relationship to check
739
+ * @param callback - Optional callback to modify the relationship query
740
+ * @returns New query builder instance with the relationship non-existence check
741
+ *
742
+ * @example
743
+ * qb.whereHasNot('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
744
+ */
745
+ whereHasNot<K extends keyof TTable['relations'] & string>(
746
+ relationName: K,
747
+ callbackOrOptions?: RelationCallback | WhereHasOptions,
748
+ maybeOptions?: WhereHasOptions
749
+ ): SelectQueryBuilder<T, TTable> {
750
+ const predicate = buildWhereHasPredicate(
751
+ this.env,
752
+ this.context,
753
+ this.relationFacet,
754
+ table => this.createChildBuilder(table),
755
+ relationName,
756
+ callbackOrOptions,
757
+ maybeOptions,
758
+ true
759
+ );
760
+
761
+ return this.where(predicate);
762
+ }
816
763
 
817
764
 
818
765