metal-orm 1.0.58 → 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 (40) hide show
  1. package/README.md +34 -31
  2. package/dist/index.cjs +1463 -1003
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +148 -129
  5. package/dist/index.d.ts +148 -129
  6. package/dist/index.js +1459 -1003
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +183 -146
  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 +13 -11
  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 -410
  21. package/src/orm/execute.ts +4 -4
  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 -492
  28. package/src/orm/relations/many-to-many.ts +2 -1
  29. package/src/query-builder/relation-cte-builder.ts +63 -0
  30. package/src/query-builder/relation-filter-utils.ts +159 -0
  31. package/src/query-builder/relation-include-strategies.ts +177 -0
  32. package/src/query-builder/relation-join-planner.ts +80 -0
  33. package/src/query-builder/relation-service.ts +119 -479
  34. package/src/query-builder/relation-types.ts +41 -10
  35. package/src/query-builder/select/projection-facet.ts +23 -23
  36. package/src/query-builder/select/select-operations.ts +145 -0
  37. package/src/query-builder/select.ts +351 -422
  38. package/src/schema/relation.ts +22 -18
  39. package/src/schema/table.ts +22 -9
  40. package/src/schema/types.ts +14 -12
@@ -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 { RelationKinds, 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,12 +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>;
79
- private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
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>;
80
89
 
81
90
  /**
82
91
  * Creates a new SelectQueryBuilder instance
@@ -92,20 +101,28 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
92
101
  dependencies?: Partial<SelectQueryBuilderDependencies>,
93
102
  lazyRelations?: Set<string>,
94
103
  lazyRelationOptions?: Map<string, RelationIncludeOptions>
95
- ) {
96
- const deps = resolveSelectQueryBuilderDependencies(dependencies);
97
- this.env = { table, deps };
98
- const initialState = state ?? deps.createState(table);
99
- const initialHydration = hydration ?? deps.createHydration(table);
100
- this.context = {
101
- state: initialState,
102
- hydration: initialHydration
103
- };
104
- this.lazyRelations = new Set(lazyRelations ?? []);
105
- this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
106
- this.columnSelector = deps.createColumnSelector(this.env);
107
- this.relationManager = deps.createRelationManager(this.env);
108
- }
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
+ }
109
126
 
110
127
  /**
111
128
  * Creates a new SelectQueryBuilder instance with updated context and lazy relations
@@ -132,15 +149,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
132
149
  * Applies an alias to the root FROM table.
133
150
  * @param alias - Alias to apply
134
151
  */
135
- as(alias: string): SelectQueryBuilder<T, TTable> {
136
- const from = this.context.state.ast.from;
137
- if (from.type !== 'Table') {
138
- throw new Error('Cannot alias non-table FROM sources');
139
- }
140
- const nextFrom = { ...from, alias };
141
- const nextContext = this.applyAst(this.context, service => service.withFrom(nextFrom));
142
- return this.clone(nextContext);
143
- }
152
+ as(alias: string): SelectQueryBuilder<T, TTable> {
153
+ const nextContext = this.fromFacet.as(this.context, alias);
154
+ return this.clone(nextContext);
155
+ }
144
156
 
145
157
 
146
158
 
@@ -168,52 +180,19 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
168
180
  return new SelectQueryBuilder(table, undefined, undefined, this.env.deps);
169
181
  }
170
182
 
171
- /**
172
- * Applies an AST mutation using the query AST service
173
- * @param context - Current query context
174
- * @param mutator - Function that mutates the AST
175
- * @returns Updated query context
176
- */
177
- private applyAst(
178
- context: SelectQueryBuilderContext,
179
- mutator: (service: QueryAstService) => SelectQueryState
180
- ): SelectQueryBuilderContext {
181
- const astService = this.env.deps.createQueryAstService(this.env.table, context.state);
182
- const nextState = mutator(astService);
183
- return { state: nextState, hydration: context.hydration };
184
- }
185
-
186
- /**
187
- * Applies a join to the query context
188
- * @param context - Current query context
189
- * @param table - Table to join
190
- * @param condition - Join condition
191
- * @param kind - Join kind
192
- * @returns Updated query context with join applied
193
- */
194
- private applyJoin(
195
- context: SelectQueryBuilderContext,
196
- table: TableDef,
197
- condition: BinaryExpressionNode,
198
- kind: JoinKind
199
- ): SelectQueryBuilderContext {
200
- const joinNode = createJoinNode(kind, { type: 'Table', name: table.name, schema: table.schema }, condition);
201
- return this.applyAst(context, service => service.withJoin(joinNode));
202
- }
203
-
204
- /**
205
- * Applies a set operation to the query
206
- * @param operator - Set operation kind
207
- * @param query - Query to combine with
208
- * @returns Updated query context with set operation
209
- */
210
- private applySetOperation<TSub extends TableDef>(
211
- operator: SetOperationKind,
212
- query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
213
- ): SelectQueryBuilderContext {
214
- const subAst = resolveSelectQuery(query);
215
- return this.applyAst(this.context, service => service.withSetOperation(operator, subAst));
216
- }
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
+ }
217
196
 
218
197
 
219
198
  /**
@@ -230,10 +209,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
230
209
  ...args: K[] | [Record<string, ColumnSelectionValue>]
231
210
  ): SelectQueryBuilder<T, TTable> {
232
211
  // If first arg is an object (not a string), treat as projection map
233
- if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
234
- const columns = args[0] as Record<string, ColumnSelectionValue>;
235
- return this.clone(this.columnSelector.select(this.context, columns));
236
- }
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
+ }
237
216
 
238
217
  // Otherwise, treat as column names
239
218
  const cols = args as K[];
@@ -246,17 +225,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
246
225
  selection[key] = col;
247
226
  }
248
227
 
249
- return this.clone(this.columnSelector.select(this.context, selection));
250
- }
228
+ return this.clone(this.projectionFacet.select(this.context, selection));
229
+ }
251
230
 
252
231
  /**
253
232
  * Selects raw column expressions
254
233
  * @param cols - Column expressions as strings
255
234
  * @returns New query builder instance with raw column selections
256
235
  */
257
- selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
258
- return this.clone(this.columnSelector.selectRaw(this.context, cols));
259
- }
236
+ selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
237
+ return this.clone(this.projectionFacet.selectRaw(this.context, cols));
238
+ }
260
239
 
261
240
  /**
262
241
  * Adds a Common Table Expression (CTE) to the query
@@ -265,11 +244,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
265
244
  * @param columns - Optional column names for the CTE
266
245
  * @returns New query builder instance with the CTE
267
246
  */
268
- with<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
269
- const subAst = resolveSelectQuery(query);
270
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, false));
271
- return this.clone(nextContext);
272
- }
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
+ }
273
252
 
274
253
  /**
275
254
  * Adds a recursive Common Table Expression (CTE) to the query
@@ -278,11 +257,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
278
257
  * @param columns - Optional column names for the CTE
279
258
  * @returns New query builder instance with the recursive CTE
280
259
  */
281
- withRecursive<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
282
- const subAst = resolveSelectQuery(query);
283
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, true));
284
- return this.clone(nextContext);
285
- }
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
+ }
286
265
 
287
266
  /**
288
267
  * Replaces the FROM clause with a derived table (subquery with alias)
@@ -291,16 +270,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
291
270
  * @param columnAliases - Optional column alias list
292
271
  * @returns New query builder instance with updated FROM
293
272
  */
294
- fromSubquery<TSub extends TableDef>(
295
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
296
- alias: string,
297
- columnAliases?: string[]
298
- ): SelectQueryBuilder<T, TTable> {
299
- const subAst = resolveSelectQuery(subquery);
300
- const fromNode = derivedTable(subAst, alias, columnAliases);
301
- const nextContext = this.applyAst(this.context, service => service.withFrom(fromNode));
302
- return this.clone(nextContext);
303
- }
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
+ }
304
282
 
305
283
  /**
306
284
  * Replaces the FROM clause with a function table expression.
@@ -309,16 +287,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
309
287
  * @param alias - Optional alias for the function table
310
288
  * @param options - Optional function-table metadata (lateral, ordinality, column aliases, schema)
311
289
  */
312
- fromFunctionTable(
313
- name: string,
314
- args: OperandNode[] = [],
315
- alias?: string,
316
- options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
317
- ): SelectQueryBuilder<T, TTable> {
318
- const functionTable = fnTable(name, args, alias, options);
319
- const nextContext = this.applyAst(this.context, service => service.withFrom(functionTable));
320
- return this.clone(nextContext);
321
- }
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
+ }
322
299
 
323
300
  /**
324
301
  * Selects a subquery as a column
@@ -326,10 +303,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
326
303
  * @param sub - Query builder or query node for the subquery
327
304
  * @returns New query builder instance with the subquery selection
328
305
  */
329
- selectSubquery<TSub extends TableDef>(alias: string, sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
330
- const query = resolveSelectQuery(sub);
331
- return this.clone(this.columnSelector.selectSubquery(this.context, alias, query));
332
- }
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
+ }
333
310
 
334
311
  /**
335
312
  * Adds a JOIN against a derived table (subquery with alias)
@@ -340,18 +317,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
340
317
  * @param columnAliases - Optional column alias list for the derived table
341
318
  * @returns New query builder instance with the derived-table join
342
319
  */
343
- joinSubquery<TSub extends TableDef>(
344
- subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
345
- alias: string,
346
- condition: BinaryExpressionNode,
347
- joinKind: JoinKind = JOIN_KINDS.INNER,
348
- columnAliases?: string[]
349
- ): SelectQueryBuilder<T, TTable> {
350
- const subAst = resolveSelectQuery(subquery);
351
- const joinNode = createJoinNode(joinKind, derivedTable(subAst, alias, columnAliases), condition);
352
- const nextContext = this.applyAst(this.context, service => service.withJoin(joinNode));
353
- return this.clone(nextContext);
354
- }
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
+ }
355
331
 
356
332
  /**
357
333
  * Adds a join against a function table (e.g., `generate_series`) using `fnTable` internally.
@@ -362,19 +338,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
362
338
  * @param joinKind - Kind of join (defaults to INNER)
363
339
  * @param options - Optional metadata (lateral, ordinality, column aliases, schema)
364
340
  */
365
- joinFunctionTable(
366
- name: string,
367
- args: OperandNode[] = [],
368
- alias: string,
369
- condition: BinaryExpressionNode,
370
- joinKind: JoinKind = JOIN_KINDS.INNER,
371
- options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
372
- ): SelectQueryBuilder<T, TTable> {
373
- const functionTable = fnTable(name, args, alias, options);
374
- const joinNode = createJoinNode(joinKind, functionTable, condition);
375
- const nextContext = this.applyAst(this.context, service => service.withJoin(joinNode));
376
- return this.clone(nextContext);
377
- }
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
+ }
378
352
 
379
353
  /**
380
354
  * Adds an INNER JOIN to the query
@@ -382,10 +356,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
382
356
  * @param condition - Join condition expression
383
357
  * @returns New query builder instance with the INNER JOIN
384
358
  */
385
- innerJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
386
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
387
- return this.clone(nextContext);
388
- }
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
+ }
389
363
 
390
364
  /**
391
365
  * Adds a LEFT JOIN to the query
@@ -393,10 +367,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
393
367
  * @param condition - Join condition expression
394
368
  * @returns New query builder instance with the LEFT JOIN
395
369
  */
396
- leftJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
397
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
398
- return this.clone(nextContext);
399
- }
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
+ }
400
374
 
401
375
  /**
402
376
  * Adds a RIGHT JOIN to the query
@@ -404,10 +378,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
404
378
  * @param condition - Join condition expression
405
379
  * @returns New query builder instance with the RIGHT JOIN
406
380
  */
407
- rightJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
408
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
409
- return this.clone(nextContext);
410
- }
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
+ }
411
385
 
412
386
  /**
413
387
  * Matches records based on a relationship
@@ -415,13 +389,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
415
389
  * @param predicate - Optional predicate expression
416
390
  * @returns New query builder instance with the relationship match
417
391
  */
418
- match<K extends keyof TTable['relations'] & string>(
419
- relationName: K,
420
- predicate?: ExpressionNode
421
- ): SelectQueryBuilder<T, TTable> {
422
- const nextContext = this.relationManager.match(this.context, relationName, predicate);
423
- return this.clone(nextContext);
424
- }
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
+ }
425
399
 
426
400
  /**
427
401
  * Joins a related table
@@ -430,14 +404,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
430
404
  * @param extraCondition - Optional additional join condition
431
405
  * @returns New query builder instance with the relationship join
432
406
  */
433
- joinRelation<K extends keyof TTable['relations'] & string>(
434
- relationName: K,
435
- joinKind: JoinKind = JOIN_KINDS.INNER,
436
- extraCondition?: ExpressionNode
437
- ): SelectQueryBuilder<T, TTable> {
438
- const nextContext = this.relationManager.joinRelation(this.context, relationName, joinKind, extraCondition);
439
- return this.clone(nextContext);
440
- }
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
+ }
441
415
 
442
416
  /**
443
417
  * Includes related data in the query results
@@ -445,13 +419,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
445
419
  * @param options - Optional include options
446
420
  * @returns New query builder instance with the relationship inclusion
447
421
  */
448
- include<K extends keyof TTable['relations'] & string>(
449
- relationName: K,
450
- options?: RelationIncludeOptions
451
- ): SelectQueryBuilder<T, TTable> {
452
- const nextContext = this.relationManager.include(this.context, relationName, options);
453
- return this.clone(nextContext);
454
- }
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
+ }
455
429
 
456
430
  /**
457
431
  * Includes a relation lazily in the query results
@@ -461,45 +435,44 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
461
435
  */
462
436
  includeLazy<K extends keyof RelationMap<TTable>>(
463
437
  relationName: K,
464
- options?: RelationIncludeOptions
438
+ options?: TypedRelationIncludeOptions<TTable['relations'][K]>
465
439
  ): SelectQueryBuilder<T, TTable> {
466
- let nextContext = this.context;
467
- const relation = this.env.table.relations[relationName as string];
468
- if (relation?.type === RelationKinds.BelongsTo) {
469
- const foreignKey = relation.foreignKey;
470
- const fkColumn = this.env.table.columns[foreignKey];
471
- if (fkColumn) {
472
- const hasAlias = nextContext.state.ast.columns.some(col => {
473
- const node = col as { alias?: string; name?: string };
474
- return (node.alias ?? node.name) === foreignKey;
475
- });
476
- if (!hasAlias) {
477
- nextContext = this.columnSelector.select(nextContext, { [foreignKey]: fkColumn });
478
- }
479
- }
480
- }
481
- const nextLazy = new Set(this.lazyRelations);
482
- nextLazy.add(relationName as string);
483
- const nextOptions = new Map(this.lazyRelationOptions);
484
- if (options) {
485
- nextOptions.set(relationName as string, options);
486
- } else {
487
- nextOptions.delete(relationName as string);
488
- }
489
- return this.clone(nextContext, nextLazy, nextOptions);
490
- }
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
+ }
455
+ const nextLazy = new Set(this.lazyRelations);
456
+ nextLazy.add(relationName as string);
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);
462
+ }
463
+ return this.clone(nextContext, nextLazy, nextOptions);
464
+ }
491
465
 
492
466
  /**
493
467
  * Convenience alias for including only specific columns from a relation.
494
468
  */
495
- includePick<
496
- K extends keyof TTable['relations'] & string,
497
- TRel extends RelationDef = TTable['relations'][K],
498
- TTarget extends TableDef = RelationTargetTable<TRel>,
499
- C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
500
- >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
501
- return this.include(relationName, { columns: cols as readonly string[] });
502
- }
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
+ }
503
476
 
504
477
 
505
478
  /**
@@ -511,13 +484,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
511
484
  // eslint-disable-next-line @typescript-eslint/no-this-alias
512
485
  let currBuilder: SelectQueryBuilder<T, TTable> = this;
513
486
 
514
- for (const entry of config) {
515
- if (entry.type === 'root') {
516
- currBuilder = currBuilder.select(...entry.columns);
517
- } else {
518
- currBuilder = currBuilder.include(entry.relationName, { columns: entry.columns as string[] });
519
- }
520
- }
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
+ }
521
495
 
522
496
  return currBuilder;
523
497
  }
@@ -555,63 +529,28 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
555
529
  return executeHydrated(ctx, this);
556
530
  }
557
531
 
558
- private withAst(ast: SelectQueryNode): SelectQueryBuilder<T, TTable> {
559
- const nextState = new SelectQueryState(this.env.table as TTable, ast);
560
- const nextContext: SelectQueryBuilderContext = {
561
- ...this.context,
562
- state: nextState
563
- };
564
- return this.clone(nextContext);
565
- }
566
-
567
- async count(session: OrmSession): Promise<number> {
568
- const unpagedAst: SelectQueryNode = {
569
- ...this.context.state.ast,
570
- orderBy: undefined,
571
- limit: undefined,
572
- offset: undefined
573
- };
574
-
575
- const subAst = this.withAst(unpagedAst).getAST();
576
-
577
- const countQuery: SelectQueryNode = {
578
- type: 'SelectQuery',
579
- from: derivedTable(subAst, '__metal_count'),
580
- columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
581
- joins: []
582
- };
583
-
584
- const execCtx = session.getExecutionContext();
585
- const compiled = execCtx.dialect.compileSelect(countQuery);
586
- const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
587
- const value = results[0]?.values?.[0]?.[0];
588
-
589
- if (typeof value === 'number') return value;
590
- if (typeof value === 'bigint') return Number(value);
591
- if (typeof value === 'string') return Number(value);
592
- return value === null || value === undefined ? 0 : Number(value);
593
- }
594
-
595
- async executePaged(
596
- session: OrmSession,
597
- options: { page: number; pageSize: number }
598
- ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
599
- const { page, pageSize } = options;
600
- if (!Number.isInteger(page) || page < 1) {
601
- throw new Error('executePaged: page must be an integer >= 1');
602
- }
603
- if (!Number.isInteger(pageSize) || pageSize < 1) {
604
- throw new Error('executePaged: pageSize must be an integer >= 1');
605
- }
606
-
607
- const offset = (page - 1) * pageSize;
608
- const [items, totalItems] = await Promise.all([
609
- this.limit(pageSize).offset(offset).execute(session),
610
- this.count(session)
611
- ]);
612
-
613
- return { items, totalItems };
614
- }
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
+ }
615
554
 
616
555
  /**
617
556
  * Executes the query with provided execution and hydration contexts
@@ -628,81 +567,79 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
628
567
  * @param expr - Expression for the WHERE clause
629
568
  * @returns New query builder instance with the WHERE condition
630
569
  */
631
- where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
632
- const nextContext = this.applyAst(this.context, service => service.withWhere(expr));
633
- return this.clone(nextContext);
634
- }
570
+ where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
571
+ const nextContext = this.predicateFacet.where(this.context, expr);
572
+ return this.clone(nextContext);
573
+ }
635
574
 
636
575
  /**
637
576
  * Adds a GROUP BY clause to the query
638
577
  * @param term - Column definition or ordering term to group by
639
578
  * @returns New query builder instance with the GROUP BY clause
640
579
  */
641
- groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
642
- const nextContext = this.applyAst(this.context, service => service.withGroupBy(term));
643
- return this.clone(nextContext);
644
- }
580
+ groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
581
+ const nextContext = this.predicateFacet.groupBy(this.context, term);
582
+ return this.clone(nextContext);
583
+ }
645
584
 
646
585
  /**
647
586
  * Adds a HAVING condition to the query
648
587
  * @param expr - Expression for the HAVING clause
649
588
  * @returns New query builder instance with the HAVING condition
650
589
  */
651
- having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
652
- const nextContext = this.applyAst(this.context, service => service.withHaving(expr));
653
- return this.clone(nextContext);
654
- }
655
-
590
+ having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
591
+ const nextContext = this.predicateFacet.having(this.context, expr);
592
+ return this.clone(nextContext);
593
+ }
656
594
 
657
595
 
658
- /**
659
- * Adds an ORDER BY clause to the query
660
- * @param term - Column definition or ordering term to order by
661
- * @param directionOrOptions - Order direction or options (defaults to ASC)
662
- * @returns New query builder instance with the ORDER BY clause
663
- */
664
- orderBy(
665
- term: ColumnDef | OrderingTerm,
666
- directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string } = ORDER_DIRECTIONS.ASC
667
- ): SelectQueryBuilder<T, TTable> {
668
- const options = typeof directionOrOptions === 'string' ? { direction: directionOrOptions } : directionOrOptions;
669
- const dir = options.direction ?? ORDER_DIRECTIONS.ASC;
670
-
671
- const nextContext = this.applyAst(this.context, service =>
672
- service.withOrderBy(term, dir, options.nulls, options.collation)
673
- );
674
596
 
675
- return this.clone(nextContext);
676
- }
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
+ }
677
614
 
678
615
  /**
679
616
  * Adds a DISTINCT clause to the query
680
617
  * @param cols - Columns to make distinct
681
618
  * @returns New query builder instance with the DISTINCT clause
682
619
  */
683
- distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
684
- return this.clone(this.columnSelector.distinct(this.context, cols));
685
- }
620
+ distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
621
+ return this.clone(this.projectionFacet.distinct(this.context, cols));
622
+ }
686
623
 
687
624
  /**
688
625
  * Adds a LIMIT clause to the query
689
626
  * @param n - Maximum number of rows to return
690
627
  * @returns New query builder instance with the LIMIT clause
691
628
  */
692
- limit(n: number): SelectQueryBuilder<T, TTable> {
693
- const nextContext = this.applyAst(this.context, service => service.withLimit(n));
694
- return this.clone(nextContext);
695
- }
629
+ limit(n: number): SelectQueryBuilder<T, TTable> {
630
+ const nextContext = this.predicateFacet.limit(this.context, n);
631
+ return this.clone(nextContext);
632
+ }
696
633
 
697
634
  /**
698
635
  * Adds an OFFSET clause to the query
699
636
  * @param n - Number of rows to skip
700
637
  * @returns New query builder instance with the OFFSET clause
701
638
  */
702
- offset(n: number): SelectQueryBuilder<T, TTable> {
703
- const nextContext = this.applyAst(this.context, service => service.withOffset(n));
704
- return this.clone(nextContext);
705
- }
639
+ offset(n: number): SelectQueryBuilder<T, TTable> {
640
+ const nextContext = this.predicateFacet.offset(this.context, n);
641
+ return this.clone(nextContext);
642
+ }
706
643
 
707
644
  /**
708
645
  * Combines this query with another using UNION
@@ -768,69 +705,61 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
768
705
  return this.where(notExists(correlated));
769
706
  }
770
707
 
771
- /**
772
- * Adds a WHERE EXISTS condition based on a relationship
773
- * @param relationName - Name of the relationship to check
774
- * @param callback - Optional callback to modify the relationship query
775
- * @returns New query builder instance with the relationship existence check
776
- */
777
- whereHas<K extends keyof TTable['relations'] & string>(
778
- relationName: K,
779
- callbackOrOptions?: RelationCallback | WhereHasOptions,
780
- maybeOptions?: WhereHasOptions
781
- ): SelectQueryBuilder<T, TTable> {
782
- const relation = this.env.table.relations[relationName];
783
-
784
- if (!relation) {
785
- throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
786
- }
787
-
788
- const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions as RelationCallback : undefined;
789
- const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as WhereHasOptions | undefined;
790
-
791
- let subQb = this.createChildBuilder<unknown, typeof relation.target>(relation.target);
792
-
793
- if (callback) {
794
- subQb = callback(subQb);
795
- }
796
-
797
- const subAst = subQb.getAST();
798
- const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst, options?.correlate);
799
-
800
- return this.where(exists(finalSubAst));
801
- }
802
-
803
- /**
804
- * Adds a WHERE NOT EXISTS condition based on a relationship
805
- * @param relationName - Name of the relationship to check
806
- * @param callback - Optional callback to modify the relationship query
807
- * @returns New query builder instance with the relationship non-existence check
808
- */
809
- whereHasNot<K extends keyof TTable['relations'] & string>(
810
- relationName: K,
811
- callbackOrOptions?: RelationCallback | WhereHasOptions,
812
- maybeOptions?: WhereHasOptions
813
- ): SelectQueryBuilder<T, TTable> {
814
- const relation = this.env.table.relations[relationName];
815
-
816
- if (!relation) {
817
- throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
818
- }
819
-
820
- const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions as RelationCallback : undefined;
821
- const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as WhereHasOptions | undefined;
822
-
823
- let subQb = this.createChildBuilder<unknown, typeof relation.target>(relation.target);
824
-
825
- if (callback) {
826
- subQb = callback(subQb);
827
- }
828
-
829
- const subAst = subQb.getAST();
830
- const finalSubAst = this.relationManager.applyRelationCorrelation(this.context, relationName, subAst, options?.correlate);
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
+ }
831
735
 
832
- return this.where(notExists(finalSubAst));
833
- }
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
+ }
834
763
 
835
764
 
836
765