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