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