metal-orm 1.0.57 → 1.0.58
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 +22 -15
- package/dist/index.cjs +640 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -45
- package/dist/index.d.ts +113 -45
- package/dist/index.js +639 -83
- package/dist/index.js.map +1 -1
- package/package.json +69 -69
- package/src/decorators/bootstrap.ts +39 -3
- package/src/orm/entity-meta.ts +6 -3
- package/src/orm/entity.ts +81 -14
- package/src/orm/execute.ts +87 -20
- package/src/orm/lazy-batch.ts +237 -54
- package/src/orm/relations/belongs-to.ts +2 -2
- package/src/orm/relations/has-many.ts +23 -9
- package/src/orm/relations/has-one.ts +2 -2
- package/src/orm/relations/many-to-many.ts +27 -13
- package/src/orm/save-graph-types.ts +2 -2
- package/src/orm/save-graph.ts +18 -18
- package/src/query-builder/relation-conditions.ts +80 -59
- package/src/query-builder/relation-service.ts +399 -95
- package/src/query-builder/relation-types.ts +2 -2
- package/src/query-builder/select.ts +58 -40
- package/src/schema/types.ts +106 -89
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { ColumnDef } from '../schema/column-types.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import {
|
|
4
|
+
RelationDef,
|
|
5
|
+
RelationKinds,
|
|
6
|
+
BelongsToManyRelation,
|
|
7
|
+
HasManyRelation,
|
|
8
|
+
HasOneRelation,
|
|
9
|
+
BelongsToRelation
|
|
10
|
+
} from '../schema/relation.js';
|
|
11
|
+
import { SelectQueryNode, TableSourceNode, TableNode, OrderingTerm } from '../core/ast/query.js';
|
|
12
|
+
import {
|
|
13
|
+
ColumnNode,
|
|
14
|
+
ExpressionNode,
|
|
15
|
+
OperandNode,
|
|
16
|
+
and,
|
|
17
|
+
isOperandNode
|
|
18
|
+
} from '../core/ast/expression.js';
|
|
10
19
|
import { SelectQueryState } from './select-query-state.js';
|
|
11
20
|
import { HydrationManager } from './hydration-manager.js';
|
|
12
21
|
import { QueryAstService } from './query-ast-service.js';
|
|
@@ -19,16 +28,29 @@ import {
|
|
|
19
28
|
buildBelongsToManyJoins
|
|
20
29
|
} from './relation-conditions.js';
|
|
21
30
|
import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
|
|
22
|
-
import { RelationIncludeOptions } from './relation-types.js';
|
|
23
|
-
import { createJoinNode } from '../core/ast/join-node.js';
|
|
24
|
-
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
25
|
-
import { makeRelationAlias } from './relation-alias.js';
|
|
26
|
-
import { buildDefaultPivotColumns } from './relation-utils.js';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
import { RelationIncludeOptions } from './relation-types.js';
|
|
32
|
+
import { createJoinNode } from '../core/ast/join-node.js';
|
|
33
|
+
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
34
|
+
import { makeRelationAlias } from './relation-alias.js';
|
|
35
|
+
import { buildDefaultPivotColumns } from './relation-utils.js';
|
|
36
|
+
|
|
37
|
+
type FilterTableCollector = {
|
|
38
|
+
tables: Set<string>;
|
|
39
|
+
hasSubquery: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type RelationWithForeignKey =
|
|
43
|
+
| HasManyRelation
|
|
44
|
+
| HasOneRelation
|
|
45
|
+
| BelongsToRelation;
|
|
46
|
+
|
|
47
|
+
const hasRelationForeignKey = (relation: RelationDef): relation is RelationWithForeignKey =>
|
|
48
|
+
relation.type !== RelationKinds.BelongsToMany;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Service for handling relation operations (joins, includes, etc.)
|
|
52
|
+
*/
|
|
53
|
+
export class RelationService {
|
|
32
54
|
private readonly projectionHelper: RelationProjectionHelper;
|
|
33
55
|
|
|
34
56
|
/**
|
|
@@ -55,14 +77,15 @@ export class RelationService {
|
|
|
55
77
|
* @param extraCondition - Additional join condition
|
|
56
78
|
* @returns Relation result with updated state and hydration
|
|
57
79
|
*/
|
|
58
|
-
joinRelation(
|
|
59
|
-
relationName: string,
|
|
60
|
-
joinKind: JoinKind,
|
|
61
|
-
extraCondition?: ExpressionNode
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
joinRelation(
|
|
81
|
+
relationName: string,
|
|
82
|
+
joinKind: JoinKind,
|
|
83
|
+
extraCondition?: ExpressionNode,
|
|
84
|
+
tableSource?: TableSourceNode
|
|
85
|
+
): RelationResult {
|
|
86
|
+
const nextState = this.withJoin(this.state, relationName, joinKind, extraCondition, tableSource);
|
|
87
|
+
return { state: nextState, hydration: this.hydration };
|
|
88
|
+
}
|
|
66
89
|
|
|
67
90
|
/**
|
|
68
91
|
* Matches records based on a relation with an optional predicate
|
|
@@ -88,26 +111,73 @@ export class RelationService {
|
|
|
88
111
|
* @param options - Options for relation inclusion
|
|
89
112
|
* @returns Relation result with updated state and hydration
|
|
90
113
|
*/
|
|
91
|
-
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
92
|
-
let state = this.state;
|
|
93
|
-
let hydration = this.hydration;
|
|
94
|
-
|
|
95
|
-
const relation = this.getRelation(relationName);
|
|
96
|
-
const aliasPrefix = options?.aliasPrefix ?? relationName;
|
|
97
|
-
const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
115
|
+
let state = this.state;
|
|
116
|
+
let hydration = this.hydration;
|
|
117
|
+
|
|
118
|
+
const relation = this.getRelation(relationName);
|
|
119
|
+
const aliasPrefix = options?.aliasPrefix ?? relationName;
|
|
120
|
+
const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
|
|
121
|
+
const { selfFilters, crossFilters } = this.splitFilterExpressions(
|
|
122
|
+
options?.filter,
|
|
123
|
+
new Set([relation.target.name])
|
|
124
|
+
);
|
|
125
|
+
const canUseCte = !alreadyJoined && selfFilters.length > 0;
|
|
126
|
+
const joinFilters = [...crossFilters];
|
|
127
|
+
if (!canUseCte) {
|
|
128
|
+
joinFilters.push(...selfFilters);
|
|
129
|
+
}
|
|
130
|
+
const joinCondition = this.combineWithAnd(joinFilters);
|
|
131
|
+
|
|
132
|
+
let tableSourceOverride: TableNode | undefined;
|
|
133
|
+
if (canUseCte) {
|
|
134
|
+
const cteInfo = this.createFilteredRelationCte(state, relationName, relation, selfFilters);
|
|
135
|
+
state = cteInfo.state;
|
|
136
|
+
tableSourceOverride = cteInfo.table;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!alreadyJoined) {
|
|
140
|
+
state = this.withJoin(
|
|
141
|
+
state,
|
|
142
|
+
relationName,
|
|
143
|
+
options?.joinKind ?? JOIN_KINDS.LEFT,
|
|
144
|
+
joinCondition,
|
|
145
|
+
tableSourceOverride
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
|
|
150
|
+
state = projectionResult.state;
|
|
151
|
+
hydration = projectionResult.hydration;
|
|
152
|
+
|
|
153
|
+
if (hasRelationForeignKey(relation)) {
|
|
154
|
+
const fkColumn = this.table.columns[relation.foreignKey];
|
|
155
|
+
if (fkColumn) {
|
|
156
|
+
const hasForeignKeySelected = state.ast.columns.some(col => {
|
|
157
|
+
if ((col as ColumnNode).type !== 'Column') return false;
|
|
158
|
+
const node = col as ColumnNode;
|
|
159
|
+
const alias = node.alias ?? node.name;
|
|
160
|
+
return alias === relation.foreignKey;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!hasForeignKeySelected) {
|
|
164
|
+
const fkSelectionResult = this.selectColumns(state, hydration, {
|
|
165
|
+
[relation.foreignKey]: fkColumn
|
|
166
|
+
});
|
|
167
|
+
state = fkSelectionResult.state;
|
|
168
|
+
hydration = fkSelectionResult.hydration;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const requestedColumns = options?.columns?.length
|
|
174
|
+
? [...options.columns]
|
|
175
|
+
: Object.keys(relation.target.columns);
|
|
176
|
+
const targetPrimaryKey = findPrimaryKey(relation.target);
|
|
177
|
+
if (!requestedColumns.includes(targetPrimaryKey)) {
|
|
178
|
+
requestedColumns.push(targetPrimaryKey);
|
|
179
|
+
}
|
|
180
|
+
const targetColumns = requestedColumns;
|
|
111
181
|
|
|
112
182
|
const buildTypedSelection = (
|
|
113
183
|
columns: Record<string, ColumnDef>,
|
|
@@ -219,36 +289,52 @@ export class RelationService {
|
|
|
219
289
|
* @param extraCondition - Additional join condition
|
|
220
290
|
* @returns Updated query state with join
|
|
221
291
|
*/
|
|
222
|
-
private withJoin(
|
|
223
|
-
state: SelectQueryState,
|
|
224
|
-
relationName: string,
|
|
225
|
-
joinKind: JoinKind,
|
|
226
|
-
extraCondition?: ExpressionNode
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
relation
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
292
|
+
private withJoin(
|
|
293
|
+
state: SelectQueryState,
|
|
294
|
+
relationName: string,
|
|
295
|
+
joinKind: JoinKind,
|
|
296
|
+
extraCondition?: ExpressionNode,
|
|
297
|
+
tableSource?: TableSourceNode
|
|
298
|
+
): SelectQueryState {
|
|
299
|
+
const relation = this.getRelation(relationName);
|
|
300
|
+
const rootAlias = state.ast.from.type === 'Table' ? state.ast.from.alias : undefined;
|
|
301
|
+
if (relation.type === RelationKinds.BelongsToMany) {
|
|
302
|
+
const targetTableSource: TableSourceNode = tableSource ?? {
|
|
303
|
+
type: 'Table',
|
|
304
|
+
name: relation.target.name,
|
|
305
|
+
schema: relation.target.schema
|
|
306
|
+
};
|
|
307
|
+
const targetName = this.resolveTargetTableName(targetTableSource, relation);
|
|
308
|
+
const joins = buildBelongsToManyJoins(
|
|
309
|
+
this.table,
|
|
310
|
+
relationName,
|
|
311
|
+
relation as BelongsToManyRelation,
|
|
312
|
+
joinKind,
|
|
313
|
+
extraCondition,
|
|
314
|
+
rootAlias,
|
|
315
|
+
targetTableSource,
|
|
316
|
+
targetName
|
|
317
|
+
);
|
|
318
|
+
return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const targetTable: TableSourceNode = tableSource ?? {
|
|
322
|
+
type: 'Table',
|
|
323
|
+
name: relation.target.name,
|
|
324
|
+
schema: relation.target.schema
|
|
325
|
+
};
|
|
326
|
+
const targetName = this.resolveTargetTableName(targetTable, relation);
|
|
327
|
+
const condition = buildRelationJoinCondition(
|
|
328
|
+
this.table,
|
|
329
|
+
relation,
|
|
330
|
+
extraCondition,
|
|
331
|
+
rootAlias,
|
|
332
|
+
targetName
|
|
333
|
+
);
|
|
334
|
+
const joinNode = createJoinNode(joinKind, targetTable, condition, relationName);
|
|
335
|
+
|
|
336
|
+
return this.astService(state).withJoin(joinNode);
|
|
337
|
+
}
|
|
252
338
|
|
|
253
339
|
/**
|
|
254
340
|
* Selects columns for a relation
|
|
@@ -257,18 +343,236 @@ export class RelationService {
|
|
|
257
343
|
* @param columns - Columns to select
|
|
258
344
|
* @returns Relation result with updated state and hydration
|
|
259
345
|
*/
|
|
260
|
-
private selectColumns(
|
|
261
|
-
state: SelectQueryState,
|
|
262
|
-
hydration: HydrationManager,
|
|
263
|
-
columns: Record<string, ColumnDef>
|
|
264
|
-
): RelationResult {
|
|
265
|
-
const { state: nextState, addedColumns } = this.astService(state).select(columns);
|
|
266
|
-
return {
|
|
267
|
-
state: nextState,
|
|
268
|
-
hydration: hydration.onColumnsSelected(nextState, addedColumns)
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
346
|
+
private selectColumns(
|
|
347
|
+
state: SelectQueryState,
|
|
348
|
+
hydration: HydrationManager,
|
|
349
|
+
columns: Record<string, ColumnDef>
|
|
350
|
+
): RelationResult {
|
|
351
|
+
const { state: nextState, addedColumns } = this.astService(state).select(columns);
|
|
352
|
+
return {
|
|
353
|
+
state: nextState,
|
|
354
|
+
hydration: hydration.onColumnsSelected(nextState, addedColumns)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
private combineWithAnd(expressions: ExpressionNode[]): ExpressionNode | undefined {
|
|
360
|
+
if (expressions.length === 0) return undefined;
|
|
361
|
+
if (expressions.length === 1) return expressions[0];
|
|
362
|
+
return {
|
|
363
|
+
type: 'LogicalExpression',
|
|
364
|
+
operator: 'AND',
|
|
365
|
+
operands: expressions
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private splitFilterExpressions(
|
|
370
|
+
filter: ExpressionNode | undefined,
|
|
371
|
+
allowedTables: Set<string>
|
|
372
|
+
): { selfFilters: ExpressionNode[]; crossFilters: ExpressionNode[] } {
|
|
373
|
+
const terms = this.flattenAnd(filter);
|
|
374
|
+
const selfFilters: ExpressionNode[] = [];
|
|
375
|
+
const crossFilters: ExpressionNode[] = [];
|
|
376
|
+
|
|
377
|
+
for (const term of terms) {
|
|
378
|
+
if (this.isExpressionSelfContained(term, allowedTables)) {
|
|
379
|
+
selfFilters.push(term);
|
|
380
|
+
} else {
|
|
381
|
+
crossFilters.push(term);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { selfFilters, crossFilters };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private flattenAnd(node?: ExpressionNode): ExpressionNode[] {
|
|
389
|
+
if (!node) return [];
|
|
390
|
+
if (node.type === 'LogicalExpression' && node.operator === 'AND') {
|
|
391
|
+
return node.operands.flatMap(operand => this.flattenAnd(operand));
|
|
392
|
+
}
|
|
393
|
+
return [node];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private isExpressionSelfContained(expr: ExpressionNode, allowedTables: Set<string>): boolean {
|
|
397
|
+
const collector = this.collectReferencedTables(expr);
|
|
398
|
+
if (collector.hasSubquery) return false;
|
|
399
|
+
if (collector.tables.size === 0) return true;
|
|
400
|
+
for (const table of collector.tables) {
|
|
401
|
+
if (!allowedTables.has(table)) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private collectReferencedTables(expr: ExpressionNode): FilterTableCollector {
|
|
409
|
+
const collector: FilterTableCollector = {
|
|
410
|
+
tables: new Set(),
|
|
411
|
+
hasSubquery: false
|
|
412
|
+
};
|
|
413
|
+
this.collectFromExpression(expr, collector);
|
|
414
|
+
return collector;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private collectFromExpression(expr: ExpressionNode, collector: FilterTableCollector): void {
|
|
418
|
+
switch (expr.type) {
|
|
419
|
+
case 'BinaryExpression':
|
|
420
|
+
this.collectFromOperand(expr.left, collector);
|
|
421
|
+
this.collectFromOperand(expr.right, collector);
|
|
422
|
+
break;
|
|
423
|
+
case 'LogicalExpression':
|
|
424
|
+
expr.operands.forEach(operand => this.collectFromExpression(operand, collector));
|
|
425
|
+
break;
|
|
426
|
+
case 'NullExpression':
|
|
427
|
+
this.collectFromOperand(expr.left, collector);
|
|
428
|
+
break;
|
|
429
|
+
case 'InExpression':
|
|
430
|
+
this.collectFromOperand(expr.left, collector);
|
|
431
|
+
if (Array.isArray(expr.right)) {
|
|
432
|
+
expr.right.forEach(value => this.collectFromOperand(value, collector));
|
|
433
|
+
} else {
|
|
434
|
+
collector.hasSubquery = true;
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
case 'ExistsExpression':
|
|
438
|
+
collector.hasSubquery = true;
|
|
439
|
+
break;
|
|
440
|
+
case 'BetweenExpression':
|
|
441
|
+
this.collectFromOperand(expr.left, collector);
|
|
442
|
+
this.collectFromOperand(expr.lower, collector);
|
|
443
|
+
this.collectFromOperand(expr.upper, collector);
|
|
444
|
+
break;
|
|
445
|
+
case 'ArithmeticExpression':
|
|
446
|
+
case 'BitwiseExpression':
|
|
447
|
+
this.collectFromOperand(expr.left, collector);
|
|
448
|
+
this.collectFromOperand(expr.right, collector);
|
|
449
|
+
break;
|
|
450
|
+
default:
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private collectFromOperand(node: OperandNode, collector: FilterTableCollector): void {
|
|
456
|
+
switch (node.type) {
|
|
457
|
+
case 'Column':
|
|
458
|
+
collector.tables.add(node.table);
|
|
459
|
+
break;
|
|
460
|
+
case 'Function':
|
|
461
|
+
node.args.forEach(arg => this.collectFromOperand(arg, collector));
|
|
462
|
+
if (node.separator) {
|
|
463
|
+
this.collectFromOperand(node.separator, collector);
|
|
464
|
+
}
|
|
465
|
+
if (node.orderBy) {
|
|
466
|
+
node.orderBy.forEach(order => this.collectFromOrderingTerm(order.term, collector));
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
case 'JsonPath':
|
|
470
|
+
this.collectFromOperand(node.column, collector);
|
|
471
|
+
break;
|
|
472
|
+
case 'ScalarSubquery':
|
|
473
|
+
collector.hasSubquery = true;
|
|
474
|
+
break;
|
|
475
|
+
case 'CaseExpression':
|
|
476
|
+
node.conditions.forEach(({ when, then }) => {
|
|
477
|
+
this.collectFromExpression(when, collector);
|
|
478
|
+
this.collectFromOperand(then, collector);
|
|
479
|
+
});
|
|
480
|
+
if (node.else) {
|
|
481
|
+
this.collectFromOperand(node.else, collector);
|
|
482
|
+
}
|
|
483
|
+
break;
|
|
484
|
+
case 'Cast':
|
|
485
|
+
this.collectFromOperand(node.expression, collector);
|
|
486
|
+
break;
|
|
487
|
+
case 'WindowFunction':
|
|
488
|
+
node.args.forEach(arg => this.collectFromOperand(arg, collector));
|
|
489
|
+
node.partitionBy?.forEach(part => this.collectFromOperand(part, collector));
|
|
490
|
+
node.orderBy?.forEach(order => this.collectFromOrderingTerm(order.term, collector));
|
|
491
|
+
break;
|
|
492
|
+
case 'Collate':
|
|
493
|
+
this.collectFromOperand(node.expression, collector);
|
|
494
|
+
break;
|
|
495
|
+
case 'ArithmeticExpression':
|
|
496
|
+
case 'BitwiseExpression':
|
|
497
|
+
this.collectFromOperand(node.left, collector);
|
|
498
|
+
this.collectFromOperand(node.right, collector);
|
|
499
|
+
break;
|
|
500
|
+
case 'Literal':
|
|
501
|
+
case 'AliasRef':
|
|
502
|
+
break;
|
|
503
|
+
default:
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private collectFromOrderingTerm(term: OrderingTerm, collector: FilterTableCollector): void {
|
|
509
|
+
if (isOperandNode(term)) {
|
|
510
|
+
this.collectFromOperand(term, collector);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.collectFromExpression(term, collector);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private createFilteredRelationCte(
|
|
517
|
+
state: SelectQueryState,
|
|
518
|
+
relationName: string,
|
|
519
|
+
relation: RelationDef,
|
|
520
|
+
filters: ExpressionNode[]
|
|
521
|
+
): { state: SelectQueryState; table: TableNode } {
|
|
522
|
+
const cteName = this.generateUniqueCteName(state, relationName);
|
|
523
|
+
const predicate = this.combineWithAnd(filters);
|
|
524
|
+
if (!predicate) {
|
|
525
|
+
throw new Error('Unable to build filter CTE without predicates.');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const columns: ColumnNode[] = Object.keys(relation.target.columns).map(name => ({
|
|
529
|
+
type: 'Column',
|
|
530
|
+
table: relation.target.name,
|
|
531
|
+
name
|
|
532
|
+
}));
|
|
533
|
+
|
|
534
|
+
const cteQuery: SelectQueryNode = {
|
|
535
|
+
type: 'SelectQuery',
|
|
536
|
+
from: { type: 'Table', name: relation.target.name, schema: relation.target.schema },
|
|
537
|
+
columns,
|
|
538
|
+
joins: [],
|
|
539
|
+
where: predicate
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const nextState = this.astService(state).withCte(cteName, cteQuery);
|
|
543
|
+
const tableNode: TableNode = {
|
|
544
|
+
type: 'Table',
|
|
545
|
+
name: cteName,
|
|
546
|
+
alias: relation.target.name
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
return { state: nextState, table: tableNode };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private generateUniqueCteName(state: SelectQueryState, relationName: string): string {
|
|
553
|
+
const existing = new Set((state.ast.ctes ?? []).map(cte => cte.name));
|
|
554
|
+
let candidate = `${relationName}__filtered`;
|
|
555
|
+
let suffix = 1;
|
|
556
|
+
while (existing.has(candidate)) {
|
|
557
|
+
candidate = `${relationName}__filtered_${suffix}`;
|
|
558
|
+
suffix += 1;
|
|
559
|
+
}
|
|
560
|
+
return candidate;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private resolveTargetTableName(target: TableSourceNode, relation: RelationDef): string {
|
|
564
|
+
if (target.type === 'Table') {
|
|
565
|
+
return target.alias ?? target.name;
|
|
566
|
+
}
|
|
567
|
+
if (target.type === 'DerivedTable') {
|
|
568
|
+
return target.alias;
|
|
569
|
+
}
|
|
570
|
+
if (target.type === 'FunctionTable') {
|
|
571
|
+
return target.alias ?? relation.target.name;
|
|
572
|
+
}
|
|
573
|
+
return relation.target.name;
|
|
574
|
+
}
|
|
575
|
+
|
|
272
576
|
/**
|
|
273
577
|
* Gets a relation definition by name
|
|
274
578
|
* @param relationName - Name of the relation
|
|
@@ -293,12 +597,12 @@ export class RelationService {
|
|
|
293
597
|
return this.createQueryAstService(this.table, state);
|
|
294
598
|
}
|
|
295
599
|
|
|
296
|
-
private rootTableName(): string {
|
|
297
|
-
const from = this.state.ast.from;
|
|
298
|
-
if (from.type === 'Table' && from.alias) return from.alias;
|
|
299
|
-
return this.table.name;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export type { RelationResult } from './relation-projection-helper.js';
|
|
600
|
+
private rootTableName(): string {
|
|
601
|
+
const from = this.state.ast.from;
|
|
602
|
+
if (from.type === 'Table' && from.alias) return from.alias;
|
|
603
|
+
return this.table.name;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export type { RelationResult } from './relation-projection-helper.js';
|
|
304
608
|
|
|
@@ -9,8 +9,8 @@ export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS
|
|
|
9
9
|
/**
|
|
10
10
|
* Options for including a relation in a query
|
|
11
11
|
*/
|
|
12
|
-
export interface RelationIncludeOptions {
|
|
13
|
-
columns?: string[];
|
|
12
|
+
export interface RelationIncludeOptions {
|
|
13
|
+
columns?: readonly string[];
|
|
14
14
|
aliasPrefix?: string;
|
|
15
15
|
filter?: ExpressionNode;
|
|
16
16
|
joinKind?: RelationIncludeJoinKind;
|