metal-orm 1.0.58 → 1.0.59
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 +1463 -1003
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -129
- package/dist/index.d.ts +148 -129
- package/dist/index.js +1459 -1003
- 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/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 +351 -422
- package/src/schema/relation.ts +22 -18
- package/src/schema/table.ts +22 -9
- package/src/schema/types.ts +14 -12
|
@@ -1,57 +1,30 @@
|
|
|
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
|
-
|
|
10
|
-
} from '
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
ExpressionNode,
|
|
15
|
-
OperandNode,
|
|
16
|
-
and,
|
|
17
|
-
isOperandNode
|
|
18
|
-
} from '../core/ast/expression.js';
|
|
19
|
-
import { SelectQueryState } from './select-query-state.js';
|
|
20
|
-
import { HydrationManager } from './hydration-manager.js';
|
|
21
|
-
import { QueryAstService } from './query-ast-service.js';
|
|
22
|
-
import { findPrimaryKey } from './hydration-planner.js';
|
|
23
|
-
import { RelationProjectionHelper } from './relation-projection-helper.js';
|
|
24
|
-
import type { RelationResult } from './relation-projection-helper.js';
|
|
25
|
-
import {
|
|
26
|
-
buildRelationJoinCondition,
|
|
27
|
-
buildRelationCorrelation,
|
|
28
|
-
buildBelongsToManyJoins
|
|
29
|
-
} from './relation-conditions.js';
|
|
30
|
-
import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
|
|
3
|
+
import { RelationDef } from '../schema/relation.js';
|
|
4
|
+
import { SelectQueryNode, TableSourceNode, TableNode } from '../core/ast/query.js';
|
|
5
|
+
import { ColumnNode, ExpressionNode, and } from '../core/ast/expression.js';
|
|
6
|
+
import { SelectQueryState } from './select-query-state.js';
|
|
7
|
+
import { HydrationManager } from './hydration-manager.js';
|
|
8
|
+
import { QueryAstService } from './query-ast-service.js';
|
|
9
|
+
import { findPrimaryKey } from './hydration-planner.js';
|
|
10
|
+
import { RelationProjectionHelper } from './relation-projection-helper.js';
|
|
11
|
+
import type { RelationResult } from './relation-projection-helper.js';
|
|
12
|
+
import { buildRelationCorrelation } from './relation-conditions.js';
|
|
13
|
+
import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
|
|
31
14
|
import { RelationIncludeOptions } from './relation-types.js';
|
|
32
|
-
import { createJoinNode } from '../core/ast/join-node.js';
|
|
33
15
|
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
*/
|
|
16
|
+
import { splitFilterExpressions } from './relation-filter-utils.js';
|
|
17
|
+
import { RelationJoinPlanner } from './relation-join-planner.js';
|
|
18
|
+
import { RelationCteBuilder } from './relation-cte-builder.js';
|
|
19
|
+
import { relationIncludeStrategies } from './relation-include-strategies.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Service for handling relation operations (joins, includes, etc.)
|
|
23
|
+
*/
|
|
53
24
|
export class RelationService {
|
|
54
|
-
private readonly projectionHelper: RelationProjectionHelper;
|
|
25
|
+
private readonly projectionHelper: RelationProjectionHelper;
|
|
26
|
+
private readonly joinPlanner: RelationJoinPlanner;
|
|
27
|
+
private readonly cteBuilder: RelationCteBuilder;
|
|
55
28
|
|
|
56
29
|
/**
|
|
57
30
|
* Creates a new RelationService instance
|
|
@@ -64,11 +37,13 @@ export class RelationService {
|
|
|
64
37
|
private readonly state: SelectQueryState,
|
|
65
38
|
private readonly hydration: HydrationManager,
|
|
66
39
|
private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
|
|
67
|
-
) {
|
|
68
|
-
this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
|
|
69
|
-
this.selectColumns(state, hydration, columns)
|
|
70
|
-
);
|
|
71
|
-
|
|
40
|
+
) {
|
|
41
|
+
this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
|
|
42
|
+
this.selectColumns(state, hydration, columns)
|
|
43
|
+
);
|
|
44
|
+
this.joinPlanner = new RelationJoinPlanner(table, createQueryAstService);
|
|
45
|
+
this.cteBuilder = new RelationCteBuilder(table, createQueryAstService);
|
|
46
|
+
}
|
|
72
47
|
|
|
73
48
|
/**
|
|
74
49
|
* Joins a relation to the query
|
|
@@ -77,13 +52,21 @@ export class RelationService {
|
|
|
77
52
|
* @param extraCondition - Additional join condition
|
|
78
53
|
* @returns Relation result with updated state and hydration
|
|
79
54
|
*/
|
|
80
|
-
joinRelation(
|
|
81
|
-
relationName: string,
|
|
82
|
-
joinKind: JoinKind,
|
|
83
|
-
extraCondition?: ExpressionNode,
|
|
84
|
-
tableSource?: TableSourceNode
|
|
55
|
+
joinRelation(
|
|
56
|
+
relationName: string,
|
|
57
|
+
joinKind: JoinKind,
|
|
58
|
+
extraCondition?: ExpressionNode,
|
|
59
|
+
tableSource?: TableSourceNode
|
|
85
60
|
): RelationResult {
|
|
86
|
-
const
|
|
61
|
+
const relation = this.getRelation(relationName);
|
|
62
|
+
const nextState = this.joinPlanner.withJoin(
|
|
63
|
+
this.state,
|
|
64
|
+
relationName,
|
|
65
|
+
relation,
|
|
66
|
+
joinKind,
|
|
67
|
+
extraCondition,
|
|
68
|
+
tableSource
|
|
69
|
+
);
|
|
87
70
|
return { state: nextState, hydration: this.hydration };
|
|
88
71
|
}
|
|
89
72
|
|
|
@@ -111,148 +94,67 @@ export class RelationService {
|
|
|
111
94
|
* @param options - Options for relation inclusion
|
|
112
95
|
* @returns Relation result with updated state and hydration
|
|
113
96
|
*/
|
|
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 } =
|
|
97
|
+
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
98
|
+
let state = this.state;
|
|
99
|
+
let hydration = this.hydration;
|
|
100
|
+
|
|
101
|
+
const relation = this.getRelation(relationName);
|
|
102
|
+
const aliasPrefix = options?.aliasPrefix ?? relationName;
|
|
103
|
+
const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
|
|
104
|
+
const { selfFilters, crossFilters } = splitFilterExpressions(
|
|
122
105
|
options?.filter,
|
|
123
106
|
new Set([relation.target.name])
|
|
124
107
|
);
|
|
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;
|
|
108
|
+
const canUseCte = !alreadyJoined && selfFilters.length > 0;
|
|
109
|
+
const joinFilters = [...crossFilters];
|
|
110
|
+
if (!canUseCte) {
|
|
111
|
+
joinFilters.push(...selfFilters);
|
|
112
|
+
}
|
|
113
|
+
const joinCondition = this.combineWithAnd(joinFilters);
|
|
114
|
+
|
|
115
|
+
let tableSourceOverride: TableNode | undefined;
|
|
133
116
|
if (canUseCte) {
|
|
134
|
-
const
|
|
117
|
+
const predicate = this.combineWithAnd(selfFilters);
|
|
118
|
+
const cteInfo = this.cteBuilder.createFilteredRelationCte(
|
|
119
|
+
state,
|
|
120
|
+
relationName,
|
|
121
|
+
relation,
|
|
122
|
+
predicate
|
|
123
|
+
);
|
|
135
124
|
state = cteInfo.state;
|
|
136
125
|
tableSourceOverride = cteInfo.table;
|
|
137
126
|
}
|
|
138
127
|
|
|
139
128
|
if (!alreadyJoined) {
|
|
140
|
-
state = this.withJoin(
|
|
129
|
+
state = this.joinPlanner.withJoin(
|
|
141
130
|
state,
|
|
142
131
|
relationName,
|
|
132
|
+
relation,
|
|
143
133
|
options?.joinKind ?? JOIN_KINDS.LEFT,
|
|
144
134
|
joinCondition,
|
|
145
135
|
tableSourceOverride
|
|
146
136
|
);
|
|
147
137
|
}
|
|
148
138
|
|
|
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;
|
|
181
|
-
|
|
182
|
-
const buildTypedSelection = (
|
|
183
|
-
columns: Record<string, ColumnDef>,
|
|
184
|
-
prefix: string,
|
|
185
|
-
keys: string[],
|
|
186
|
-
missingMsg: (col: string) => string
|
|
187
|
-
): Record<string, ColumnDef> => {
|
|
188
|
-
return keys.reduce((acc, key) => {
|
|
189
|
-
const def = columns[key];
|
|
190
|
-
if (!def) {
|
|
191
|
-
throw new Error(missingMsg(key));
|
|
192
|
-
}
|
|
193
|
-
acc[makeRelationAlias(prefix, key)] = def;
|
|
194
|
-
return acc;
|
|
195
|
-
}, {} as Record<string, ColumnDef>);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const targetSelection = buildTypedSelection(
|
|
199
|
-
relation.target.columns as Record<string, ColumnDef>,
|
|
200
|
-
aliasPrefix,
|
|
201
|
-
targetColumns,
|
|
202
|
-
key => `Column '${key}' not found on relation '${relationName}'`
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
if (relation.type !== RelationKinds.BelongsToMany) {
|
|
206
|
-
const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
|
|
207
|
-
state = relationSelectionResult.state;
|
|
208
|
-
hydration = relationSelectionResult.hydration;
|
|
209
|
-
|
|
210
|
-
hydration = hydration.onRelationIncluded(
|
|
211
|
-
state,
|
|
212
|
-
relation,
|
|
213
|
-
relationName,
|
|
214
|
-
aliasPrefix,
|
|
215
|
-
targetColumns
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
return { state, hydration };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const many = relation as BelongsToManyRelation;
|
|
222
|
-
const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
|
|
223
|
-
const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
|
|
224
|
-
const pivotColumns =
|
|
225
|
-
options?.pivot?.columns ??
|
|
226
|
-
many.defaultPivotColumns ??
|
|
227
|
-
buildDefaultPivotColumns(many, pivotPk);
|
|
139
|
+
const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
|
|
140
|
+
state = projectionResult.state;
|
|
141
|
+
hydration = projectionResult.hydration;
|
|
228
142
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
state
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
hydration = hydration.onRelationIncluded(
|
|
246
|
-
state,
|
|
247
|
-
relation,
|
|
248
|
-
relationName,
|
|
249
|
-
aliasPrefix,
|
|
250
|
-
targetColumns,
|
|
251
|
-
{ aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
return { state, hydration };
|
|
255
|
-
}
|
|
143
|
+
const strategy = relationIncludeStrategies[relation.type];
|
|
144
|
+
const result = strategy({
|
|
145
|
+
rootTable: this.table,
|
|
146
|
+
state,
|
|
147
|
+
hydration,
|
|
148
|
+
relation,
|
|
149
|
+
relationName,
|
|
150
|
+
aliasPrefix,
|
|
151
|
+
options,
|
|
152
|
+
selectColumns: (nextState, nextHydration, columns) =>
|
|
153
|
+
this.selectColumns(nextState, nextHydration, columns)
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return { state: result.state, hydration: result.hydration };
|
|
157
|
+
}
|
|
256
158
|
|
|
257
159
|
/**
|
|
258
160
|
* Applies relation correlation to a query AST
|
|
@@ -281,298 +183,36 @@ export class RelationService {
|
|
|
281
183
|
};
|
|
282
184
|
}
|
|
283
185
|
|
|
284
|
-
/**
|
|
285
|
-
*
|
|
286
|
-
* @param state - Current query state
|
|
287
|
-
* @param relationName - Name of the relation
|
|
288
|
-
* @param joinKind - Type of join to use
|
|
289
|
-
* @param extraCondition - Additional join condition
|
|
290
|
-
* @returns Updated query state with join
|
|
291
|
-
*/
|
|
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
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Selects columns for a relation
|
|
186
|
+
/**
|
|
187
|
+
* Selects columns for a relation
|
|
341
188
|
* @param state - Current query state
|
|
342
189
|
* @param hydration - Hydration manager
|
|
343
190
|
* @param columns - Columns to select
|
|
344
191
|
* @returns Relation result with updated state and hydration
|
|
345
192
|
*/
|
|
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
|
-
|
|
193
|
+
private selectColumns(
|
|
194
|
+
state: SelectQueryState,
|
|
195
|
+
hydration: HydrationManager,
|
|
196
|
+
columns: Record<string, ColumnDef>
|
|
197
|
+
): RelationResult {
|
|
198
|
+
const { state: nextState, addedColumns } = this.astService(state).select(columns);
|
|
199
|
+
return {
|
|
200
|
+
state: nextState,
|
|
201
|
+
hydration: hydration.onColumnsSelected(nextState, addedColumns)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
private combineWithAnd(expressions: ExpressionNode[]): ExpressionNode | undefined {
|
|
207
|
+
if (expressions.length === 0) return undefined;
|
|
208
|
+
if (expressions.length === 1) return expressions[0];
|
|
209
|
+
return {
|
|
210
|
+
type: 'LogicalExpression',
|
|
211
|
+
operator: 'AND',
|
|
212
|
+
operands: expressions
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
576
216
|
/**
|
|
577
217
|
* Gets a relation definition by name
|
|
578
218
|
* @param relationName - Name of the relation
|
|
@@ -597,12 +237,12 @@ export class RelationService {
|
|
|
597
237
|
return this.createQueryAstService(this.table, state);
|
|
598
238
|
}
|
|
599
239
|
|
|
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';
|
|
240
|
+
private rootTableName(): string {
|
|
241
|
+
const from = this.state.ast.from;
|
|
242
|
+
if (from.type === 'Table' && from.alias) return from.alias;
|
|
243
|
+
return this.table.name;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type { RelationResult } from './relation-projection-helper.js';
|
|
608
248
|
|