metal-orm 1.0.59 → 1.0.62

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