metal-orm 1.0.58 → 1.0.60

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 (41) hide show
  1. package/README.md +34 -31
  2. package/dist/index.cjs +1583 -901
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +400 -129
  5. package/dist/index.d.ts +400 -129
  6. package/dist/index.js +1575 -901
  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/index.ts +7 -7
  16. package/src/orm/entity-hydration.ts +72 -0
  17. package/src/orm/entity-meta.ts +13 -11
  18. package/src/orm/entity-metadata.ts +240 -238
  19. package/src/orm/entity-relation-cache.ts +39 -0
  20. package/src/orm/entity-relations.ts +207 -0
  21. package/src/orm/entity.ts +124 -410
  22. package/src/orm/execute.ts +4 -4
  23. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  24. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  25. package/src/orm/lazy-batch/has-many.ts +69 -0
  26. package/src/orm/lazy-batch/has-one.ts +68 -0
  27. package/src/orm/lazy-batch/shared.ts +125 -0
  28. package/src/orm/lazy-batch.ts +4 -492
  29. package/src/orm/relations/many-to-many.ts +2 -1
  30. package/src/query-builder/relation-cte-builder.ts +63 -0
  31. package/src/query-builder/relation-filter-utils.ts +159 -0
  32. package/src/query-builder/relation-include-strategies.ts +177 -0
  33. package/src/query-builder/relation-join-planner.ts +80 -0
  34. package/src/query-builder/relation-service.ts +119 -479
  35. package/src/query-builder/relation-types.ts +41 -10
  36. package/src/query-builder/select/projection-facet.ts +23 -23
  37. package/src/query-builder/select/select-operations.ts +145 -0
  38. package/src/query-builder/select.ts +329 -221
  39. package/src/schema/relation.ts +22 -18
  40. package/src/schema/table.ts +22 -9
  41. package/src/schema/types.ts +14 -12
@@ -14,7 +14,6 @@ import {
14
14
  notExists,
15
15
  OperandNode
16
16
  } from '../core/ast/expression.js';
17
- import { derivedTable, fnTable } from '../core/ast/builders.js';
18
17
  import { CompiledQuery, Dialect } from '../core/dialect/abstract.js';
19
18
  import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
20
19
 
@@ -28,20 +27,31 @@ import {
28
27
  SelectQueryBuilderDependencies,
29
28
  SelectQueryBuilderEnvironment
30
29
  } from './select-query-builder-deps.js';
31
- import { QueryAstService } from './query-ast-service.js';
32
30
  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';
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';
34
+ import { EntityInstance, RelationMap } from '../schema/types.js';
38
35
  import { OrmSession } from '../orm/orm-session.ts';
39
36
  import { ExecutionContext } from '../orm/execution-context.js';
40
37
  import { HydrationContext } from '../orm/hydration-context.js';
41
38
  import { executeHydrated, executeHydratedWithContexts } from '../orm/execute.js';
42
- import { createJoinNode } from '../core/ast/join-node.js';
43
39
  import { resolveSelectQuery } from './query-resolution.js';
44
-
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';
45
55
 
46
56
  type ColumnSelectionValue = ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode;
47
57
 
@@ -56,15 +66,6 @@ type DeepSelectEntry<TTable extends TableDef> = {
56
66
 
57
67
  type DeepSelectConfig<TTable extends TableDef> = DeepSelectEntry<TTable>[];
58
68
 
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
-
68
69
  /**
69
70
  * Main query builder class for constructing SQL SELECT queries
70
71
  * @typeParam T - Result type for projections (unused)
@@ -74,7 +75,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
74
75
  private readonly env: SelectQueryBuilderEnvironment;
75
76
  private readonly context: SelectQueryBuilderContext;
76
77
  private readonly columnSelector: ColumnSelector;
77
- private readonly relationManager: RelationManager;
78
+ private readonly fromFacet: SelectFromFacet;
79
+ private readonly joinFacet: SelectJoinFacet;
80
+ private readonly projectionFacet: SelectProjectionFacet;
81
+ private readonly predicateFacet: SelectPredicateFacet;
82
+ private readonly cteFacet: SelectCTEFacet;
83
+ private readonly setOpFacet: SelectSetOpFacet;
84
+ private readonly relationFacet: SelectRelationFacet;
78
85
  private readonly lazyRelations: Set<string>;
79
86
  private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
80
87
 
@@ -95,6 +102,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
95
102
  ) {
96
103
  const deps = resolveSelectQueryBuilderDependencies(dependencies);
97
104
  this.env = { table, deps };
105
+ const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
98
106
  const initialState = state ?? deps.createState(table);
99
107
  const initialHydration = hydration ?? deps.createHydration(table);
100
108
  this.context = {
@@ -104,7 +112,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
104
112
  this.lazyRelations = new Set(lazyRelations ?? []);
105
113
  this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
106
114
  this.columnSelector = deps.createColumnSelector(this.env);
107
- this.relationManager = deps.createRelationManager(this.env);
115
+ const relationManager = deps.createRelationManager(this.env);
116
+ this.fromFacet = new SelectFromFacet(this.env, createAstService);
117
+ this.joinFacet = new SelectJoinFacet(this.env, createAstService);
118
+ this.projectionFacet = new SelectProjectionFacet(this.columnSelector);
119
+ this.predicateFacet = new SelectPredicateFacet(this.env, createAstService);
120
+ this.cteFacet = new SelectCTEFacet(this.env, createAstService);
121
+ this.setOpFacet = new SelectSetOpFacet(this.env, createAstService);
122
+ this.relationFacet = new SelectRelationFacet(relationManager);
108
123
  }
109
124
 
110
125
  /**
@@ -131,19 +146,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
131
146
  /**
132
147
  * Applies an alias to the root FROM table.
133
148
  * @param alias - Alias to apply
149
+ * @example
150
+ * const qb = new SelectQueryBuilder(userTable).as('u');
134
151
  */
135
152
  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));
153
+ const nextContext = this.fromFacet.as(this.context, alias);
142
154
  return this.clone(nextContext);
143
155
  }
144
156
 
145
-
146
-
147
157
  /**
148
158
  * Applies correlation expression to the query AST
149
159
  * @param ast - Query AST to modify
@@ -168,39 +178,6 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
168
178
  return new SelectQueryBuilder(table, undefined, undefined, this.env.deps);
169
179
  }
170
180
 
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
181
  /**
205
182
  * Applies a set operation to the query
206
183
  * @param operator - Set operation kind
@@ -212,15 +189,23 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
212
189
  query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
213
190
  ): SelectQueryBuilderContext {
214
191
  const subAst = resolveSelectQuery(query);
215
- return this.applyAst(this.context, service => service.withSetOperation(operator, subAst));
192
+ return this.setOpFacet.applySetOperation(this.context, operator, subAst);
216
193
  }
217
194
 
218
-
219
195
  /**
220
196
  * Selects columns for the query (unified overloaded method).
221
197
  * Can be called with column names or a projection object.
222
198
  * @param args - Column names or projection object
223
199
  * @returns New query builder instance with selected columns
200
+ * @example
201
+ * // Select specific columns
202
+ * qb.select('id', 'name', 'email');
203
+ * @example
204
+ * // Select with aliases and expressions
205
+ * qb.select({
206
+ * id: userTable.columns.id,
207
+ * fullName: concat(userTable.columns.firstName, ' ', userTable.columns.lastName)
208
+ * });
224
209
  */
225
210
  select<K extends keyof TTable['columns'] & string>(
226
211
  ...args: K[]
@@ -232,7 +217,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
232
217
  // If first arg is an object (not a string), treat as projection map
233
218
  if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && typeof args[0] !== 'string') {
234
219
  const columns = args[0] as Record<string, ColumnSelectionValue>;
235
- return this.clone(this.columnSelector.select(this.context, columns));
220
+ return this.clone(this.projectionFacet.select(this.context, columns));
236
221
  }
237
222
 
238
223
  // Otherwise, treat as column names
@@ -246,16 +231,18 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
246
231
  selection[key] = col;
247
232
  }
248
233
 
249
- return this.clone(this.columnSelector.select(this.context, selection));
234
+ return this.clone(this.projectionFacet.select(this.context, selection));
250
235
  }
251
236
 
252
237
  /**
253
238
  * Selects raw column expressions
254
239
  * @param cols - Column expressions as strings
255
240
  * @returns New query builder instance with raw column selections
241
+ * @example
242
+ * qb.selectRaw('COUNT(*) as total', 'UPPER(name) as upper_name');
256
243
  */
257
244
  selectRaw(...cols: string[]): SelectQueryBuilder<T, TTable> {
258
- return this.clone(this.columnSelector.selectRaw(this.context, cols));
245
+ return this.clone(this.projectionFacet.selectRaw(this.context, cols));
259
246
  }
260
247
 
261
248
  /**
@@ -264,10 +251,16 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
264
251
  * @param query - Query builder or query node for the CTE
265
252
  * @param columns - Optional column names for the CTE
266
253
  * @returns New query builder instance with the CTE
254
+ * @example
255
+ * const recentUsers = new SelectQueryBuilder(userTable)
256
+ * .where(gt(userTable.columns.createdAt, subDays(now(), 30)));
257
+ * const qb = new SelectQueryBuilder(userTable)
258
+ * .with('recent_users', recentUsers)
259
+ * .from('recent_users');
267
260
  */
268
261
  with<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
269
262
  const subAst = resolveSelectQuery(query);
270
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, false));
263
+ const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, false);
271
264
  return this.clone(nextContext);
272
265
  }
273
266
 
@@ -277,10 +270,23 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
277
270
  * @param query - Query builder or query node for the CTE
278
271
  * @param columns - Optional column names for the CTE
279
272
  * @returns New query builder instance with the recursive CTE
273
+ * @example
274
+ * // Base case: select root nodes
275
+ * const baseQuery = new SelectQueryBuilder(orgTable)
276
+ * .where(eq(orgTable.columns.parentId, 1));
277
+ * // Recursive case: join with the CTE itself
278
+ * const recursiveQuery = new SelectQueryBuilder(orgTable)
279
+ * .join('org_hierarchy', 'oh', eq(orgTable.columns.parentId, col('oh.id')));
280
+ * // Combine base and recursive parts
281
+ * const orgHierarchy = baseQuery.union(recursiveQuery);
282
+ * // Use in main query
283
+ * const qb = new SelectQueryBuilder(orgTable)
284
+ * .withRecursive('org_hierarchy', orgHierarchy)
285
+ * .from('org_hierarchy');
280
286
  */
281
287
  withRecursive<TSub extends TableDef>(name: string, query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode, columns?: string[]): SelectQueryBuilder<T, TTable> {
282
288
  const subAst = resolveSelectQuery(query);
283
- const nextContext = this.applyAst(this.context, service => service.withCte(name, subAst, columns, true));
289
+ const nextContext = this.cteFacet.withCTE(this.context, name, subAst, columns, true);
284
290
  return this.clone(nextContext);
285
291
  }
286
292
 
@@ -290,6 +296,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
290
296
  * @param alias - Alias for the derived table
291
297
  * @param columnAliases - Optional column alias list
292
298
  * @returns New query builder instance with updated FROM
299
+ * @example
300
+ * const subquery = new SelectQueryBuilder(userTable)
301
+ * .select('id', 'name')
302
+ * .where(gt(userTable.columns.score, 100));
303
+ * qb.fromSubquery(subquery, 'high_scorers', ['userId', 'userName']);
293
304
  */
294
305
  fromSubquery<TSub extends TableDef>(
295
306
  subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
@@ -297,8 +308,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
297
308
  columnAliases?: string[]
298
309
  ): SelectQueryBuilder<T, TTable> {
299
310
  const subAst = resolveSelectQuery(subquery);
300
- const fromNode = derivedTable(subAst, alias, columnAliases);
301
- const nextContext = this.applyAst(this.context, service => service.withFrom(fromNode));
311
+ const nextContext = this.fromFacet.fromSubquery(this.context, subAst, alias, columnAliases);
302
312
  return this.clone(nextContext);
303
313
  }
304
314
 
@@ -308,6 +318,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
308
318
  * @param args - Optional function arguments
309
319
  * @param alias - Optional alias for the function table
310
320
  * @param options - Optional function-table metadata (lateral, ordinality, column aliases, schema)
321
+ * @example
322
+ * qb.fromFunctionTable(
323
+ * 'generate_series',
324
+ * [literal(1), literal(10), literal(1)],
325
+ * 'series',
326
+ * { columnAliases: ['value'] }
327
+ * );
311
328
  */
312
329
  fromFunctionTable(
313
330
  name: string,
@@ -315,8 +332,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
315
332
  alias?: string,
316
333
  options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
317
334
  ): SelectQueryBuilder<T, TTable> {
318
- const functionTable = fnTable(name, args, alias, options);
319
- const nextContext = this.applyAst(this.context, service => service.withFrom(functionTable));
335
+ const nextContext = this.fromFacet.fromFunctionTable(this.context, name, args, alias, options);
320
336
  return this.clone(nextContext);
321
337
  }
322
338
 
@@ -325,10 +341,16 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
325
341
  * @param alias - Alias for the subquery column
326
342
  * @param sub - Query builder or query node for the subquery
327
343
  * @returns New query builder instance with the subquery selection
344
+ * @example
345
+ * const postCount = new SelectQueryBuilder(postTable)
346
+ * .select(count(postTable.columns.id))
347
+ * .where(eq(postTable.columns.userId, col('u.id')));
348
+ * qb.select('id', 'name')
349
+ * .selectSubquery('postCount', postCount);
328
350
  */
329
351
  selectSubquery<TSub extends TableDef>(alias: string, sub: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
330
352
  const query = resolveSelectQuery(sub);
331
- return this.clone(this.columnSelector.selectSubquery(this.context, alias, query));
353
+ return this.clone(this.projectionFacet.selectSubquery(this.context, alias, query));
332
354
  }
333
355
 
334
356
  /**
@@ -339,6 +361,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
339
361
  * @param joinKind - Join kind (defaults to INNER)
340
362
  * @param columnAliases - Optional column alias list for the derived table
341
363
  * @returns New query builder instance with the derived-table join
364
+ * @example
365
+ * const activeUsers = new SelectQueryBuilder(userTable)
366
+ * .where(eq(userTable.columns.active, true));
367
+ * qb.joinSubquery(
368
+ * activeUsers,
369
+ * 'au',
370
+ * eq(col('t.userId'), col('au.id')),
371
+ * JOIN_KINDS.LEFT
372
+ * );
342
373
  */
343
374
  joinSubquery<TSub extends TableDef>(
344
375
  subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
@@ -348,8 +379,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
348
379
  columnAliases?: string[]
349
380
  ): SelectQueryBuilder<T, TTable> {
350
381
  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));
382
+ const nextContext = this.joinFacet.joinSubquery(this.context, subAst, alias, condition, joinKind, columnAliases);
353
383
  return this.clone(nextContext);
354
384
  }
355
385
 
@@ -361,6 +391,15 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
361
391
  * @param condition - Join condition expression
362
392
  * @param joinKind - Kind of join (defaults to INNER)
363
393
  * @param options - Optional metadata (lateral, ordinality, column aliases, schema)
394
+ * @example
395
+ * qb.joinFunctionTable(
396
+ * 'generate_series',
397
+ * [literal(1), literal(10)],
398
+ * 'gs',
399
+ * eq(col('t.value'), col('gs.value')),
400
+ * JOIN_KINDS.INNER,
401
+ * { columnAliases: ['value'] }
402
+ * );
364
403
  */
365
404
  joinFunctionTable(
366
405
  name: string,
@@ -370,9 +409,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
370
409
  joinKind: JoinKind = JOIN_KINDS.INNER,
371
410
  options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
372
411
  ): 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));
412
+ const nextContext = this.joinFacet.joinFunctionTable(this.context, name, args, alias, condition, joinKind, options);
376
413
  return this.clone(nextContext);
377
414
  }
378
415
 
@@ -381,9 +418,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
381
418
  * @param table - Table to join
382
419
  * @param condition - Join condition expression
383
420
  * @returns New query builder instance with the INNER JOIN
421
+ * @example
422
+ * qb.innerJoin(
423
+ * postTable,
424
+ * eq(userTable.columns.id, postTable.columns.userId)
425
+ * );
384
426
  */
385
427
  innerJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
386
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
428
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.INNER);
387
429
  return this.clone(nextContext);
388
430
  }
389
431
 
@@ -392,9 +434,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
392
434
  * @param table - Table to join
393
435
  * @param condition - Join condition expression
394
436
  * @returns New query builder instance with the LEFT JOIN
437
+ * @example
438
+ * qb.leftJoin(
439
+ * postTable,
440
+ * eq(userTable.columns.id, postTable.columns.userId)
441
+ * );
395
442
  */
396
443
  leftJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
397
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
444
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.LEFT);
398
445
  return this.clone(nextContext);
399
446
  }
400
447
 
@@ -403,9 +450,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
403
450
  * @param table - Table to join
404
451
  * @param condition - Join condition expression
405
452
  * @returns New query builder instance with the RIGHT JOIN
453
+ * @example
454
+ * qb.rightJoin(
455
+ * postTable,
456
+ * eq(userTable.columns.id, postTable.columns.userId)
457
+ * );
406
458
  */
407
459
  rightJoin(table: TableDef, condition: BinaryExpressionNode): SelectQueryBuilder<T, TTable> {
408
- const nextContext = this.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
460
+ const nextContext = this.joinFacet.applyJoin(this.context, table, condition, JOIN_KINDS.RIGHT);
409
461
  return this.clone(nextContext);
410
462
  }
411
463
 
@@ -414,12 +466,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
414
466
  * @param relationName - Name of the relationship to match
415
467
  * @param predicate - Optional predicate expression
416
468
  * @returns New query builder instance with the relationship match
469
+ * @example
470
+ * qb.match('posts', eq(postTable.columns.published, true));
417
471
  */
418
472
  match<K extends keyof TTable['relations'] & string>(
419
473
  relationName: K,
420
474
  predicate?: ExpressionNode
421
475
  ): SelectQueryBuilder<T, TTable> {
422
- const nextContext = this.relationManager.match(this.context, relationName, predicate);
476
+ const nextContext = this.relationFacet.match(this.context, relationName, predicate);
423
477
  return this.clone(nextContext);
424
478
  }
425
479
 
@@ -429,13 +483,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
429
483
  * @param joinKind - Type of join (defaults to INNER)
430
484
  * @param extraCondition - Optional additional join condition
431
485
  * @returns New query builder instance with the relationship join
486
+ * @example
487
+ * qb.joinRelation('posts', JOIN_KINDS.LEFT);
488
+ * @example
489
+ * qb.joinRelation('posts', JOIN_KINDS.INNER, eq(postTable.columns.published, true));
432
490
  */
433
491
  joinRelation<K extends keyof TTable['relations'] & string>(
434
492
  relationName: K,
435
493
  joinKind: JoinKind = JOIN_KINDS.INNER,
436
494
  extraCondition?: ExpressionNode
437
495
  ): SelectQueryBuilder<T, TTable> {
438
- const nextContext = this.relationManager.joinRelation(this.context, relationName, joinKind, extraCondition);
496
+ const nextContext = this.relationFacet.joinRelation(this.context, relationName, joinKind, extraCondition);
439
497
  return this.clone(nextContext);
440
498
  }
441
499
 
@@ -444,12 +502,21 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
444
502
  * @param relationName - Name of the relationship to include
445
503
  * @param options - Optional include options
446
504
  * @returns New query builder instance with the relationship inclusion
505
+ * @example
506
+ * qb.include('posts');
507
+ * @example
508
+ * qb.include('posts', { columns: ['id', 'title', 'published'] });
509
+ * @example
510
+ * qb.include('posts', {
511
+ * columns: ['id', 'title'],
512
+ * where: eq(postTable.columns.published, true)
513
+ * });
447
514
  */
448
515
  include<K extends keyof TTable['relations'] & string>(
449
516
  relationName: K,
450
- options?: RelationIncludeOptions
517
+ options?: TypedRelationIncludeOptions<TTable['relations'][K]>
451
518
  ): SelectQueryBuilder<T, TTable> {
452
- const nextContext = this.relationManager.include(this.context, relationName, options);
519
+ const nextContext = this.relationFacet.include(this.context, relationName, options);
453
520
  return this.clone(nextContext);
454
521
  }
455
522
 
@@ -458,54 +525,64 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
458
525
  * @param relationName - Name of the relation to include lazily
459
526
  * @param options - Optional include options for lazy loading
460
527
  * @returns New query builder instance with lazy relation inclusion
528
+ * @example
529
+ * const qb = new SelectQueryBuilder(userTable).includeLazy('posts');
530
+ * const users = await qb.execute(session);
531
+ * // Access posts later - they will be loaded on demand
532
+ * const posts = await users[0].posts;
461
533
  */
462
- includeLazy<K extends keyof RelationMap<TTable>>(
463
- relationName: K,
464
- options?: RelationIncludeOptions
465
- ): 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
- }
534
+ includeLazy<K extends keyof RelationMap<TTable>>(
535
+ relationName: K,
536
+ options?: TypedRelationIncludeOptions<TTable['relations'][K]>
537
+ ): SelectQueryBuilder<T, TTable> {
538
+ let nextContext = this.context;
539
+ const relation = this.env.table.relations[relationName as string];
540
+ if (relation?.type === RelationKinds.BelongsTo) {
541
+ const foreignKey = relation.foreignKey;
542
+ const fkColumn = this.env.table.columns[foreignKey];
543
+ if (fkColumn) {
544
+ const hasAlias = nextContext.state.ast.columns.some(col => {
545
+ const node = col as { alias?: string; name?: string };
546
+ return (node.alias ?? node.name) === foreignKey;
547
+ });
548
+ if (!hasAlias) {
549
+ nextContext = this.columnSelector.select(nextContext, { [foreignKey]: fkColumn });
550
+ }
551
+ }
552
+ }
553
+ const nextLazy = new Set(this.lazyRelations);
554
+ nextLazy.add(relationName as string);
555
+ const nextOptions = new Map(this.lazyRelationOptions);
556
+ if (options) {
557
+ nextOptions.set(relationName as string, options);
558
+ } else {
559
+ nextOptions.delete(relationName as string);
560
+ }
561
+ return this.clone(nextContext, nextLazy, nextOptions);
562
+ }
491
563
 
492
564
  /**
493
565
  * Convenience alias for including only specific columns from a relation.
566
+ * @example
567
+ * qb.includePick('posts', ['id', 'title', 'createdAt']);
494
568
  */
495
569
  includePick<
496
570
  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
571
+ C extends RelationTargetColumns<TTable['relations'][K]>
500
572
  >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
501
- return this.include(relationName, { columns: cols as readonly string[] });
573
+ const options = { columns: cols as readonly C[] } as unknown as TypedRelationIncludeOptions<TTable['relations'][K]>;
574
+ return this.include(relationName, options);
502
575
  }
503
576
 
504
-
505
577
  /**
506
578
  * Selects columns for the root table and relations from an array of entries
507
579
  * @param config - Configuration array for deep column selection
508
580
  * @returns New query builder instance with deep column selections
581
+ * @example
582
+ * qb.selectColumnsDeep([
583
+ * { type: 'root', columns: ['id', 'name'] },
584
+ * { type: 'relation', relationName: 'posts', columns: ['id', 'title'] }
585
+ * ]);
509
586
  */
510
587
  selectColumnsDeep(config: DeepSelectConfig<TTable>): SelectQueryBuilder<T, TTable> {
511
588
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -515,7 +592,8 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
515
592
  if (entry.type === 'root') {
516
593
  currBuilder = currBuilder.select(...entry.columns);
517
594
  } else {
518
- currBuilder = currBuilder.include(entry.relationName, { columns: entry.columns as string[] });
595
+ const options = { columns: entry.columns } as unknown as TypedRelationIncludeOptions<TTable['relations'][typeof entry.relationName]>;
596
+ currBuilder = currBuilder.include(entry.relationName, options);
519
597
  }
520
598
  }
521
599
 
@@ -550,67 +628,36 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
550
628
  * Executes the query and returns hydrated results
551
629
  * @param ctx - ORM session context
552
630
  * @returns Promise of entity instances
631
+ * @example
632
+ * const users = await qb.select('id', 'name')
633
+ * .where(eq(userTable.columns.active, true))
634
+ * .execute(session);
553
635
  */
554
636
  async execute(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
555
637
  return executeHydrated(ctx, this);
556
638
  }
557
639
 
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
-
640
+ /**
641
+ * Executes a count query for the current builder without LIMIT/OFFSET clauses.
642
+ *
643
+ * @example
644
+ * const total = await qb.count(session);
645
+ */
567
646
  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);
647
+ return executeCount(this.context, this.env, session);
593
648
  }
594
649
 
650
+ /**
651
+ * Executes the query and returns both the paged items and the total.
652
+ *
653
+ * @example
654
+ * const { items, totalItems } = await qb.executePaged(session, { page: 1, pageSize: 20 });
655
+ */
595
656
  async executePaged(
596
657
  session: OrmSession,
597
658
  options: { page: number; pageSize: number }
598
659
  ): 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 };
660
+ return executePagedQuery(this, session, options, sess => this.count(sess));
614
661
  }
615
662
 
616
663
  /**
@@ -618,6 +665,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
618
665
  * @param execCtx - Execution context
619
666
  * @param hydCtx - Hydration context
620
667
  * @returns Promise of entity instances
668
+ * @example
669
+ * const execCtx = new ExecutionContext(session);
670
+ * const hydCtx = new HydrationContext();
671
+ * const users = await qb.executeWithContexts(execCtx, hydCtx);
621
672
  */
622
673
  async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<EntityInstance<TTable>[]> {
623
674
  return executeHydratedWithContexts(execCtx, hydCtx, this);
@@ -627,9 +678,16 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
627
678
  * Adds a WHERE condition to the query
628
679
  * @param expr - Expression for the WHERE clause
629
680
  * @returns New query builder instance with the WHERE condition
681
+ * @example
682
+ * qb.where(eq(userTable.columns.id, 1));
683
+ * @example
684
+ * qb.where(and(
685
+ * eq(userTable.columns.active, true),
686
+ * gt(userTable.columns.createdAt, subDays(now(), 30))
687
+ * ));
630
688
  */
631
689
  where(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
632
- const nextContext = this.applyAst(this.context, service => service.withWhere(expr));
690
+ const nextContext = this.predicateFacet.where(this.context, expr);
633
691
  return this.clone(nextContext);
634
692
  }
635
693
 
@@ -637,9 +695,12 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
637
695
  * Adds a GROUP BY clause to the query
638
696
  * @param term - Column definition or ordering term to group by
639
697
  * @returns New query builder instance with the GROUP BY clause
698
+ * @example
699
+ * qb.select('departmentId', count(userTable.columns.id))
700
+ * .groupBy(userTable.columns.departmentId);
640
701
  */
641
702
  groupBy(term: ColumnDef | OrderingTerm): SelectQueryBuilder<T, TTable> {
642
- const nextContext = this.applyAst(this.context, service => service.withGroupBy(term));
703
+ const nextContext = this.predicateFacet.groupBy(this.context, term);
643
704
  return this.clone(nextContext);
644
705
  }
645
706
 
@@ -647,30 +708,30 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
647
708
  * Adds a HAVING condition to the query
648
709
  * @param expr - Expression for the HAVING clause
649
710
  * @returns New query builder instance with the HAVING condition
711
+ * @example
712
+ * qb.select('departmentId', count(userTable.columns.id))
713
+ * .groupBy(userTable.columns.departmentId)
714
+ * .having(gt(count(userTable.columns.id), 5));
650
715
  */
651
716
  having(expr: ExpressionNode): SelectQueryBuilder<T, TTable> {
652
- const nextContext = this.applyAst(this.context, service => service.withHaving(expr));
717
+ const nextContext = this.predicateFacet.having(this.context, expr);
653
718
  return this.clone(nextContext);
654
719
  }
655
720
 
656
-
657
-
658
721
  /**
659
722
  * Adds an ORDER BY clause to the query
660
723
  * @param term - Column definition or ordering term to order by
661
724
  * @param directionOrOptions - Order direction or options (defaults to ASC)
662
725
  * @returns New query builder instance with the ORDER BY clause
726
+ *
727
+ * @example
728
+ * qb.orderBy(userTable.columns.createdAt, 'DESC');
663
729
  */
664
730
  orderBy(
665
731
  term: ColumnDef | OrderingTerm,
666
732
  directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string } = ORDER_DIRECTIONS.ASC
667
733
  ): 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
- );
734
+ const nextContext = applyOrderBy(this.context, this.predicateFacet, term, directionOrOptions);
674
735
 
675
736
  return this.clone(nextContext);
676
737
  }
@@ -679,18 +740,26 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
679
740
  * Adds a DISTINCT clause to the query
680
741
  * @param cols - Columns to make distinct
681
742
  * @returns New query builder instance with the DISTINCT clause
743
+ * @example
744
+ * qb.distinct(userTable.columns.email);
745
+ * @example
746
+ * qb.distinct(userTable.columns.firstName, userTable.columns.lastName);
682
747
  */
683
748
  distinct(...cols: (ColumnDef | ColumnNode)[]): SelectQueryBuilder<T, TTable> {
684
- return this.clone(this.columnSelector.distinct(this.context, cols));
749
+ return this.clone(this.projectionFacet.distinct(this.context, cols));
685
750
  }
686
751
 
687
752
  /**
688
753
  * Adds a LIMIT clause to the query
689
754
  * @param n - Maximum number of rows to return
690
755
  * @returns New query builder instance with the LIMIT clause
756
+ * @example
757
+ * qb.limit(10);
758
+ * @example
759
+ * qb.limit(20).offset(40); // Pagination: page 3 with 20 items per page
691
760
  */
692
761
  limit(n: number): SelectQueryBuilder<T, TTable> {
693
- const nextContext = this.applyAst(this.context, service => service.withLimit(n));
762
+ const nextContext = this.predicateFacet.limit(this.context, n);
694
763
  return this.clone(nextContext);
695
764
  }
696
765
 
@@ -698,9 +767,13 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
698
767
  * Adds an OFFSET clause to the query
699
768
  * @param n - Number of rows to skip
700
769
  * @returns New query builder instance with the OFFSET clause
770
+ * @example
771
+ * qb.offset(10);
772
+ * @example
773
+ * qb.limit(20).offset(40); // Pagination: page 3 with 20 items per page
701
774
  */
702
775
  offset(n: number): SelectQueryBuilder<T, TTable> {
703
- const nextContext = this.applyAst(this.context, service => service.withOffset(n));
776
+ const nextContext = this.predicateFacet.offset(this.context, n);
704
777
  return this.clone(nextContext);
705
778
  }
706
779
 
@@ -708,6 +781,12 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
708
781
  * Combines this query with another using UNION
709
782
  * @param query - Query to union with
710
783
  * @returns New query builder instance with the set operation
784
+ * @example
785
+ * const activeUsers = new SelectQueryBuilder(userTable)
786
+ * .where(eq(userTable.columns.active, true));
787
+ * const inactiveUsers = new SelectQueryBuilder(userTable)
788
+ * .where(eq(userTable.columns.active, false));
789
+ * qb.union(activeUsers).union(inactiveUsers);
711
790
  */
712
791
  union<TSub extends TableDef>(query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
713
792
  return this.clone(this.applySetOperation('UNION', query));
@@ -717,6 +796,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
717
796
  * Combines this query with another using UNION ALL
718
797
  * @param query - Query to union with
719
798
  * @returns New query builder instance with the set operation
799
+ * @example
800
+ * const q1 = new SelectQueryBuilder(userTable).where(gt(userTable.columns.score, 80));
801
+ * const q2 = new SelectQueryBuilder(userTable).where(lt(userTable.columns.score, 20));
802
+ * qb.unionAll(q1).unionAll(q2);
720
803
  */
721
804
  unionAll<TSub extends TableDef>(query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
722
805
  return this.clone(this.applySetOperation('UNION ALL', query));
@@ -726,6 +809,12 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
726
809
  * Combines this query with another using INTERSECT
727
810
  * @param query - Query to intersect with
728
811
  * @returns New query builder instance with the set operation
812
+ * @example
813
+ * const activeUsers = new SelectQueryBuilder(userTable)
814
+ * .where(eq(userTable.columns.active, true));
815
+ * const premiumUsers = new SelectQueryBuilder(userTable)
816
+ * .where(eq(userTable.columns.premium, true));
817
+ * qb.intersect(activeUsers).intersect(premiumUsers);
729
818
  */
730
819
  intersect<TSub extends TableDef>(query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
731
820
  return this.clone(this.applySetOperation('INTERSECT', query));
@@ -735,6 +824,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
735
824
  * Combines this query with another using EXCEPT
736
825
  * @param query - Query to subtract
737
826
  * @returns New query builder instance with the set operation
827
+ * @example
828
+ * const allUsers = new SelectQueryBuilder(userTable);
829
+ * const inactiveUsers = new SelectQueryBuilder(userTable)
830
+ * .where(eq(userTable.columns.active, false));
831
+ * qb.except(allUsers).except(inactiveUsers); // Only active users
738
832
  */
739
833
  except<TSub extends TableDef>(query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
740
834
  return this.clone(this.applySetOperation('EXCEPT', query));
@@ -744,6 +838,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
744
838
  * Adds a WHERE EXISTS condition to the query
745
839
  * @param subquery - Subquery to check for existence
746
840
  * @returns New query builder instance with the WHERE EXISTS condition
841
+ * @example
842
+ * const postsQuery = new SelectQueryBuilder(postTable)
843
+ * .where(eq(postTable.columns.userId, col('u.id')));
844
+ * qb.whereExists(postsQuery);
747
845
  */
748
846
  whereExists<TSub extends TableDef>(
749
847
  subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
@@ -758,6 +856,10 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
758
856
  * Adds a WHERE NOT EXISTS condition to the query
759
857
  * @param subquery - Subquery to check for non-existence
760
858
  * @returns New query builder instance with the WHERE NOT EXISTS condition
859
+ * @example
860
+ * const postsQuery = new SelectQueryBuilder(postTable)
861
+ * .where(eq(postTable.columns.userId, col('u.id')));
862
+ * qb.whereNotExists(postsQuery); // Users without posts
761
863
  */
762
864
  whereNotExists<TSub extends TableDef>(
763
865
  subquery: SelectQueryBuilder<unknown, TSub> | SelectQueryNode,
@@ -773,31 +875,27 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
773
875
  * @param relationName - Name of the relationship to check
774
876
  * @param callback - Optional callback to modify the relationship query
775
877
  * @returns New query builder instance with the relationship existence check
878
+ *
879
+ * @example
880
+ * qb.whereHas('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
776
881
  */
777
882
  whereHas<K extends keyof TTable['relations'] & string>(
778
883
  relationName: K,
779
884
  callbackOrOptions?: RelationCallback | WhereHasOptions,
780
885
  maybeOptions?: WhereHasOptions
781
886
  ): 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);
887
+ const predicate = buildWhereHasPredicate(
888
+ this.env,
889
+ this.context,
890
+ this.relationFacet,
891
+ table => this.createChildBuilder(table),
892
+ relationName,
893
+ callbackOrOptions,
894
+ maybeOptions,
895
+ false
896
+ );
799
897
 
800
- return this.where(exists(finalSubAst));
898
+ return this.where(predicate);
801
899
  }
802
900
 
803
901
  /**
@@ -805,39 +903,38 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
805
903
  * @param relationName - Name of the relationship to check
806
904
  * @param callback - Optional callback to modify the relationship query
807
905
  * @returns New query builder instance with the relationship non-existence check
906
+ *
907
+ * @example
908
+ * qb.whereHasNot('posts', postQb => postQb.where(eq(postTable.columns.published, true)));
808
909
  */
809
910
  whereHasNot<K extends keyof TTable['relations'] & string>(
810
911
  relationName: K,
811
912
  callbackOrOptions?: RelationCallback | WhereHasOptions,
812
913
  maybeOptions?: WhereHasOptions
813
914
  ): 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);
915
+ const predicate = buildWhereHasPredicate(
916
+ this.env,
917
+ this.context,
918
+ this.relationFacet,
919
+ table => this.createChildBuilder(table),
920
+ relationName,
921
+ callbackOrOptions,
922
+ maybeOptions,
923
+ true
924
+ );
831
925
 
832
- return this.where(notExists(finalSubAst));
926
+ return this.where(predicate);
833
927
  }
834
928
 
835
-
836
-
837
929
  /**
838
930
  * Compiles the query to SQL for a specific dialect
839
931
  * @param dialect - Database dialect to compile for
840
932
  * @returns Compiled query with SQL and parameters
933
+ * @example
934
+ * const compiled = qb.select('id', 'name')
935
+ * .where(eq(userTable.columns.active, true))
936
+ * .compile('postgres');
937
+ * console.log(compiled.sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
841
938
  */
842
939
  compile(dialect: SelectDialectInput): CompiledQuery {
843
940
  const resolved = resolveDialectInput(dialect);
@@ -848,6 +945,11 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
848
945
  * Converts the query to SQL string for a specific dialect
849
946
  * @param dialect - Database dialect to generate SQL for
850
947
  * @returns SQL string representation of the query
948
+ * @example
949
+ * const sql = qb.select('id', 'name')
950
+ * .where(eq(userTable.columns.active, true))
951
+ * .toSql('postgres');
952
+ * console.log(sql); // SELECT "id", "name" FROM "users" WHERE "active" = true
851
953
  */
852
954
  toSql(dialect: SelectDialectInput): string {
853
955
  return this.compile(dialect).sql;
@@ -856,6 +958,9 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
856
958
  /**
857
959
  * Gets the hydration plan for the query
858
960
  * @returns Hydration plan or undefined if none exists
961
+ * @example
962
+ * const plan = qb.include('posts').getHydrationPlan();
963
+ * console.log(plan?.relations); // Information about included relations
859
964
  */
860
965
  getHydrationPlan(): HydrationPlan | undefined {
861
966
  return this.context.hydration.getPlan();
@@ -864,9 +969,12 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
864
969
  /**
865
970
  * Gets the Abstract Syntax Tree (AST) representation of the query
866
971
  * @returns Query AST with hydration applied
972
+ * @example
973
+ * const ast = qb.select('id', 'name').getAST();
974
+ * console.log(ast.columns); // Array of column nodes
975
+ * console.log(ast.from); // From clause information
867
976
  */
868
977
  getAST(): SelectQueryNode {
869
978
  return this.context.hydration.applyToAst(this.context.state.ast);
870
979
  }
871
980
  }
872
-