metal-orm 1.0.8 → 1.0.10
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 +341 -146
- package/dist/decorators/index.cjs +2564 -0
- package/dist/decorators/index.cjs.map +1 -0
- package/dist/decorators/index.d.cts +53 -0
- package/dist/decorators/index.d.ts +53 -0
- package/dist/decorators/index.js +2530 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +701 -0
- package/dist/index.d.ts +701 -0
- package/dist/index.js +4131 -0
- package/dist/index.js.map +1 -0
- package/dist/select-654m4qy8.d.cts +1522 -0
- package/dist/select-654m4qy8.d.ts +1522 -0
- package/package.json +27 -20
- package/src/codegen/typescript.ts +405 -393
- package/src/core/ast/aggregate-functions.ts +30 -0
- package/src/core/ast/builders.ts +43 -0
- package/src/core/ast/expression-builders.ts +310 -0
- package/src/core/ast/expression-nodes.ts +211 -0
- package/src/core/ast/expression-visitor.ts +99 -0
- package/src/core/ast/expression.ts +5 -0
- package/src/{utils → core/ast}/join-node.ts +20 -20
- package/src/{ast → core/ast}/join.ts +18 -18
- package/src/{ast → core/ast}/query.ts +113 -113
- package/src/core/ast/window-functions.ts +140 -0
- package/src/{dialect → core/dialect}/abstract.ts +94 -94
- package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
- package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
- package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
- package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
- package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
- package/src/decorators/bootstrap.ts +126 -0
- package/src/decorators/column.ts +78 -0
- package/src/decorators/entity.ts +36 -0
- package/src/decorators/index.ts +4 -0
- package/src/decorators/relations.ts +107 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +22 -22
- package/src/orm/db-executor.ts +11 -0
- package/src/orm/domain-event-bus.ts +52 -0
- package/src/{runtime → orm}/entity-meta.ts +52 -52
- package/src/orm/entity-metadata.ts +140 -0
- package/src/{runtime → orm}/entity.ts +252 -252
- package/src/{runtime → orm}/execute.ts +36 -36
- package/src/{runtime → orm}/hydration.ts +103 -103
- package/src/orm/identity-map.ts +37 -0
- package/src/{runtime → orm}/lazy-batch.ts +205 -205
- package/src/orm/orm-context.ts +154 -0
- package/src/orm/relation-change-processor.ts +140 -0
- package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
- package/src/{runtime → orm}/relations/has-many.ts +111 -111
- package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
- package/src/orm/runtime-types.ts +39 -0
- package/src/orm/transaction-runner.ts +17 -0
- package/src/orm/unit-of-work.ts +232 -0
- package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
- package/src/{builder → query-builder}/delete-query-state.ts +38 -42
- package/src/{builder → query-builder}/delete.ts +46 -57
- package/src/{builder → query-builder}/hydration-manager.ts +87 -87
- package/src/{builder → query-builder}/hydration-planner.ts +182 -182
- package/src/{builder → query-builder}/insert-query-state.ts +51 -62
- package/src/{builder → query-builder}/insert.ts +48 -59
- package/src/{builder → query-builder}/query-ast-service.ts +208 -226
- package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
- package/src/{builder → query-builder}/relation-conditions.ts +112 -112
- package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
- package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
- package/src/{builder → query-builder}/relation-service.ts +284 -284
- package/src/{builder → query-builder}/relation-types.ts +21 -21
- package/src/{builder → query-builder}/relation-utils.ts +12 -12
- package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
- package/src/{builder → query-builder}/select-query-state.ts +179 -179
- package/src/{builder → query-builder}/select.ts +78 -69
- package/src/{builder → query-builder}/update-query-state.ts +55 -59
- package/src/{builder → query-builder}/update.ts +50 -61
- package/src/schema/column.ts +25 -25
- package/src/schema/relation.ts +116 -116
- package/src/schema/table.ts +34 -34
- package/src/schema/types.ts +76 -76
- package/.github/workflows/publish-metal-orm.yml +0 -38
- package/ROADMAP.md +0 -125
- package/docs/CHANGES.md +0 -104
- package/docs/advanced-features.md +0 -176
- package/docs/api-reference.md +0 -31
- package/docs/dml-operations.md +0 -156
- package/docs/getting-started.md +0 -171
- package/docs/hydration.md +0 -115
- package/docs/index.md +0 -36
- package/docs/multi-dialect-support.md +0 -59
- package/docs/query-builder.md +0 -135
- package/docs/runtime.md +0 -105
- package/docs/schema-definition.md +0 -112
- package/metadata.json +0 -5
- package/playground/api/playground-api.ts +0 -94
- package/playground/index.html +0 -15
- package/playground/src/App.css +0 -1
- package/playground/src/App.tsx +0 -114
- package/playground/src/components/CodeDisplay.tsx +0 -43
- package/playground/src/components/QueryExecutor.tsx +0 -189
- package/playground/src/components/ResultsTable.tsx +0 -67
- package/playground/src/components/ResultsTabs.tsx +0 -105
- package/playground/src/components/ScenarioList.tsx +0 -56
- package/playground/src/components/logo.svg +0 -45
- package/playground/src/data/scenarios.ts +0 -2
- package/playground/src/main.tsx +0 -9
- package/playground/src/services/PlaygroundApiService.ts +0 -60
- package/postcss.config.cjs +0 -5
- package/sql_sql-ansi-cheatsheet-2025.md +0 -264
- package/src/ast/expression.ts +0 -658
- package/src/builder/operations/cte-manager.ts +0 -34
- package/src/builder/operations/filter-manager.ts +0 -68
- package/src/builder/operations/join-manager.ts +0 -36
- package/src/builder/operations/pagination-manager.ts +0 -36
- package/src/playground/features/playground/api/types.ts +0 -16
- package/src/playground/features/playground/clients/MockClient.ts +0 -17
- package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
- package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
- package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
- package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
- package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
- package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
- package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
- package/src/playground/features/playground/data/scenarios/index.ts +0 -29
- package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
- package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
- package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
- package/src/playground/features/playground/data/scenarios/types.ts +0 -70
- package/src/playground/features/playground/data/schema.ts +0 -91
- package/src/playground/features/playground/data/seed.ts +0 -104
- package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
- package/src/runtime/orm-context.ts +0 -539
- package/tests/belongs-to-many.test.ts +0 -57
- package/tests/between.test.ts +0 -43
- package/tests/case-expression.test.ts +0 -58
- package/tests/complex-exists.test.ts +0 -230
- package/tests/cte.test.ts +0 -118
- package/tests/dml.test.ts +0 -206
- package/tests/exists.test.ts +0 -127
- package/tests/like.test.ts +0 -33
- package/tests/orm-runtime.test.ts +0 -254
- package/tests/postgres.test.ts +0 -30
- package/tests/right-join.test.ts +0 -89
- package/tests/subquery-having.test.ts +0 -193
- package/tests/window-function.test.ts +0 -151
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -10
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -14
- /package/src/{constants → core/sql}/sql.ts +0 -0
- /package/src/{runtime → orm}/als.ts +0 -0
- /package/src/{utils → query-builder}/relation-alias.ts +0 -0
|
@@ -1,284 +1,284 @@
|
|
|
1
|
-
import { TableDef } from '../schema/table';
|
|
2
|
-
import { ColumnDef } from '../schema/column';
|
|
3
|
-
import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
|
|
4
|
-
import { SelectQueryNode } from '../ast/query';
|
|
5
|
-
import {
|
|
6
|
-
ColumnNode,
|
|
7
|
-
ExpressionNode,
|
|
8
|
-
and
|
|
9
|
-
} from '../ast/expression';
|
|
10
|
-
import { SelectQueryState } from './select-query-state';
|
|
11
|
-
import { HydrationManager } from './hydration-manager';
|
|
12
|
-
import { QueryAstService } from './query-ast-service';
|
|
13
|
-
import { findPrimaryKey } from './hydration-planner';
|
|
14
|
-
import { RelationProjectionHelper } from './relation-projection-helper';
|
|
15
|
-
import type { RelationResult } from './relation-projection-helper';
|
|
16
|
-
import {
|
|
17
|
-
buildRelationJoinCondition,
|
|
18
|
-
buildRelationCorrelation,
|
|
19
|
-
buildBelongsToManyJoins
|
|
20
|
-
} from './relation-conditions';
|
|
21
|
-
import { JoinKind, JOIN_KINDS } from '../
|
|
22
|
-
import { RelationIncludeOptions } from './relation-types';
|
|
23
|
-
import { createJoinNode } from '../
|
|
24
|
-
import { makeRelationAlias } from '
|
|
25
|
-
import { buildDefaultPivotColumns } from './relation-utils';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Service for handling relation operations (joins, includes, etc.)
|
|
29
|
-
*/
|
|
30
|
-
export class RelationService {
|
|
31
|
-
private readonly projectionHelper: RelationProjectionHelper;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Creates a new RelationService instance
|
|
35
|
-
* @param table - Table definition
|
|
36
|
-
* @param state - Current query state
|
|
37
|
-
* @param hydration - Hydration manager
|
|
38
|
-
*/
|
|
39
|
-
constructor(
|
|
40
|
-
private readonly table: TableDef,
|
|
41
|
-
private readonly state: SelectQueryState,
|
|
42
|
-
private readonly hydration: HydrationManager,
|
|
43
|
-
private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
|
|
44
|
-
) {
|
|
45
|
-
this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
|
|
46
|
-
this.selectColumns(state, hydration, columns)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Joins a relation to the query
|
|
52
|
-
* @param relationName - Name of the relation to join
|
|
53
|
-
* @param joinKind - Type of join to use
|
|
54
|
-
* @param extraCondition - Additional join condition
|
|
55
|
-
* @returns Relation result with updated state and hydration
|
|
56
|
-
*/
|
|
57
|
-
joinRelation(
|
|
58
|
-
relationName: string,
|
|
59
|
-
joinKind: JoinKind,
|
|
60
|
-
extraCondition?: ExpressionNode
|
|
61
|
-
): RelationResult {
|
|
62
|
-
const nextState = this.withJoin(this.state, relationName, joinKind, extraCondition);
|
|
63
|
-
return { state: nextState, hydration: this.hydration };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Matches records based on a relation with an optional predicate
|
|
68
|
-
* @param relationName - Name of the relation to match
|
|
69
|
-
* @param predicate - Optional predicate expression
|
|
70
|
-
* @returns Relation result with updated state and hydration
|
|
71
|
-
*/
|
|
72
|
-
match(
|
|
73
|
-
relationName: string,
|
|
74
|
-
predicate?: ExpressionNode
|
|
75
|
-
): RelationResult {
|
|
76
|
-
const joined = this.joinRelation(relationName, JOIN_KINDS.INNER, predicate);
|
|
77
|
-
const pk = findPrimaryKey(this.table);
|
|
78
|
-
const distinctCols: ColumnNode[] = [{ type: 'Column', table: this.table.name, name: pk }];
|
|
79
|
-
const existingDistinct = joined.state.ast.distinct ? joined.state.ast.distinct : [];
|
|
80
|
-
const nextState = this.astService(joined.state).withDistinct([...existingDistinct, ...distinctCols]);
|
|
81
|
-
return { state: nextState, hydration: joined.hydration };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Includes a relation in the query result
|
|
86
|
-
* @param relationName - Name of the relation to include
|
|
87
|
-
* @param options - Options for relation inclusion
|
|
88
|
-
* @returns Relation result with updated state and hydration
|
|
89
|
-
*/
|
|
90
|
-
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
91
|
-
let state = this.state;
|
|
92
|
-
let hydration = this.hydration;
|
|
93
|
-
|
|
94
|
-
const relation = this.getRelation(relationName);
|
|
95
|
-
const aliasPrefix = options?.aliasPrefix ?? relationName;
|
|
96
|
-
const alreadyJoined = state.ast.joins.some(j => j.relationName === relationName);
|
|
97
|
-
|
|
98
|
-
if (!alreadyJoined) {
|
|
99
|
-
const joined = this.joinRelation(relationName, options?.joinKind ?? JOIN_KINDS.LEFT, options?.filter);
|
|
100
|
-
state = joined.state;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
|
|
104
|
-
state = projectionResult.state;
|
|
105
|
-
hydration = projectionResult.hydration;
|
|
106
|
-
|
|
107
|
-
const targetColumns = options?.columns?.length
|
|
108
|
-
? options.columns
|
|
109
|
-
: Object.keys(relation.target.columns);
|
|
110
|
-
|
|
111
|
-
const buildTypedSelection = (
|
|
112
|
-
columns: Record<string, ColumnDef>,
|
|
113
|
-
prefix: string,
|
|
114
|
-
keys: string[],
|
|
115
|
-
missingMsg: (col: string) => string
|
|
116
|
-
) : Record<string, ColumnDef> => {
|
|
117
|
-
return keys.reduce((acc, key) => {
|
|
118
|
-
const def = columns[key];
|
|
119
|
-
if (!def) {
|
|
120
|
-
throw new Error(missingMsg(key));
|
|
121
|
-
}
|
|
122
|
-
acc[makeRelationAlias(prefix, key)] = def;
|
|
123
|
-
return acc;
|
|
124
|
-
}, {} as Record<string, ColumnDef>);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const targetSelection = buildTypedSelection(
|
|
128
|
-
relation.target.columns as Record<string, ColumnDef>,
|
|
129
|
-
aliasPrefix,
|
|
130
|
-
targetColumns,
|
|
131
|
-
key => `Column '${key}' not found on relation '${relationName}'`
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
if (relation.type !== RelationKinds.BelongsToMany) {
|
|
135
|
-
const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
|
|
136
|
-
state = relationSelectionResult.state;
|
|
137
|
-
hydration = relationSelectionResult.hydration;
|
|
138
|
-
|
|
139
|
-
hydration = hydration.onRelationIncluded(
|
|
140
|
-
state,
|
|
141
|
-
relation,
|
|
142
|
-
relationName,
|
|
143
|
-
aliasPrefix,
|
|
144
|
-
targetColumns
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
return { state, hydration };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const many = relation as BelongsToManyRelation;
|
|
151
|
-
const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
|
|
152
|
-
const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
|
|
153
|
-
const pivotColumns =
|
|
154
|
-
options?.pivot?.columns ??
|
|
155
|
-
many.defaultPivotColumns ??
|
|
156
|
-
buildDefaultPivotColumns(many, pivotPk);
|
|
157
|
-
|
|
158
|
-
const pivotSelection = buildTypedSelection(
|
|
159
|
-
many.pivotTable.columns as Record<string, ColumnDef>,
|
|
160
|
-
pivotAliasPrefix,
|
|
161
|
-
pivotColumns,
|
|
162
|
-
key => `Column '${key}' not found on pivot table '${many.pivotTable.name}'`
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
const combinedSelection = {
|
|
166
|
-
...targetSelection,
|
|
167
|
-
...pivotSelection
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const relationSelectionResult = this.selectColumns(state, hydration, combinedSelection);
|
|
171
|
-
state = relationSelectionResult.state;
|
|
172
|
-
hydration = relationSelectionResult.hydration;
|
|
173
|
-
|
|
174
|
-
hydration = hydration.onRelationIncluded(
|
|
175
|
-
state,
|
|
176
|
-
relation,
|
|
177
|
-
relationName,
|
|
178
|
-
aliasPrefix,
|
|
179
|
-
targetColumns,
|
|
180
|
-
{ aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
return { state, hydration };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Applies relation correlation to a query AST
|
|
188
|
-
* @param relationName - Name of the relation
|
|
189
|
-
* @param ast - Query AST to modify
|
|
190
|
-
* @returns Modified query AST with relation correlation
|
|
191
|
-
*/
|
|
192
|
-
applyRelationCorrelation(
|
|
193
|
-
relationName: string,
|
|
194
|
-
ast: SelectQueryNode
|
|
195
|
-
): SelectQueryNode {
|
|
196
|
-
const relation = this.getRelation(relationName);
|
|
197
|
-
const correlation = buildRelationCorrelation(this.table, relation);
|
|
198
|
-
const whereInSubquery = ast.where
|
|
199
|
-
? and(correlation, ast.where)
|
|
200
|
-
: correlation;
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
...ast,
|
|
204
|
-
where: whereInSubquery
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Creates a join node for a relation
|
|
210
|
-
* @param state - Current query state
|
|
211
|
-
* @param relationName - Name of the relation
|
|
212
|
-
* @param joinKind - Type of join to use
|
|
213
|
-
* @param extraCondition - Additional join condition
|
|
214
|
-
* @returns Updated query state with join
|
|
215
|
-
*/
|
|
216
|
-
private withJoin(
|
|
217
|
-
state: SelectQueryState,
|
|
218
|
-
relationName: string,
|
|
219
|
-
joinKind: JoinKind,
|
|
220
|
-
extraCondition?: ExpressionNode
|
|
221
|
-
): SelectQueryState {
|
|
222
|
-
const relation = this.getRelation(relationName);
|
|
223
|
-
if (relation.type === RelationKinds.BelongsToMany) {
|
|
224
|
-
const joins = buildBelongsToManyJoins(
|
|
225
|
-
this.table,
|
|
226
|
-
relationName,
|
|
227
|
-
relation as BelongsToManyRelation,
|
|
228
|
-
joinKind,
|
|
229
|
-
extraCondition
|
|
230
|
-
);
|
|
231
|
-
return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
|
|
235
|
-
const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
|
|
236
|
-
|
|
237
|
-
return this.astService(state).withJoin(joinNode);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Selects columns for a relation
|
|
242
|
-
* @param state - Current query state
|
|
243
|
-
* @param hydration - Hydration manager
|
|
244
|
-
* @param columns - Columns to select
|
|
245
|
-
* @returns Relation result with updated state and hydration
|
|
246
|
-
*/
|
|
247
|
-
private selectColumns(
|
|
248
|
-
state: SelectQueryState,
|
|
249
|
-
hydration: HydrationManager,
|
|
250
|
-
columns: Record<string, ColumnDef>
|
|
251
|
-
): RelationResult {
|
|
252
|
-
const { state: nextState, addedColumns } = this.astService(state).select(columns);
|
|
253
|
-
return {
|
|
254
|
-
state: nextState,
|
|
255
|
-
hydration: hydration.onColumnsSelected(nextState, addedColumns)
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Gets a relation definition by name
|
|
261
|
-
* @param relationName - Name of the relation
|
|
262
|
-
* @returns Relation definition
|
|
263
|
-
* @throws Error if relation is not found
|
|
264
|
-
*/
|
|
265
|
-
private getRelation(relationName: string): RelationDef {
|
|
266
|
-
const relation = this.table.relations[relationName];
|
|
267
|
-
if (!relation) {
|
|
268
|
-
throw new Error(`Relation '${relationName}' not found on table '${this.table.name}'`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return relation;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Creates a QueryAstService instance
|
|
276
|
-
* @param state - Current query state
|
|
277
|
-
* @returns QueryAstService instance
|
|
278
|
-
*/
|
|
279
|
-
private astService(state: SelectQueryState = this.state): QueryAstService {
|
|
280
|
-
return this.createQueryAstService(this.table, state);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export type { RelationResult } from './relation-projection-helper';
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { ColumnDef } from '../schema/column.js';
|
|
3
|
+
import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
|
|
4
|
+
import { SelectQueryNode } from '../core/ast/query.js';
|
|
5
|
+
import {
|
|
6
|
+
ColumnNode,
|
|
7
|
+
ExpressionNode,
|
|
8
|
+
and
|
|
9
|
+
} from '../core/ast/expression.js';
|
|
10
|
+
import { SelectQueryState } from './select-query-state.js';
|
|
11
|
+
import { HydrationManager } from './hydration-manager.js';
|
|
12
|
+
import { QueryAstService } from './query-ast-service.js';
|
|
13
|
+
import { findPrimaryKey } from './hydration-planner.js';
|
|
14
|
+
import { RelationProjectionHelper } from './relation-projection-helper.js';
|
|
15
|
+
import type { RelationResult } from './relation-projection-helper.js';
|
|
16
|
+
import {
|
|
17
|
+
buildRelationJoinCondition,
|
|
18
|
+
buildRelationCorrelation,
|
|
19
|
+
buildBelongsToManyJoins
|
|
20
|
+
} from './relation-conditions.js';
|
|
21
|
+
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 { makeRelationAlias } from './relation-alias.js';
|
|
25
|
+
import { buildDefaultPivotColumns } from './relation-utils.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Service for handling relation operations (joins, includes, etc.)
|
|
29
|
+
*/
|
|
30
|
+
export class RelationService {
|
|
31
|
+
private readonly projectionHelper: RelationProjectionHelper;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new RelationService instance
|
|
35
|
+
* @param table - Table definition
|
|
36
|
+
* @param state - Current query state
|
|
37
|
+
* @param hydration - Hydration manager
|
|
38
|
+
*/
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly table: TableDef,
|
|
41
|
+
private readonly state: SelectQueryState,
|
|
42
|
+
private readonly hydration: HydrationManager,
|
|
43
|
+
private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
|
|
44
|
+
) {
|
|
45
|
+
this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
|
|
46
|
+
this.selectColumns(state, hydration, columns)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Joins a relation to the query
|
|
52
|
+
* @param relationName - Name of the relation to join
|
|
53
|
+
* @param joinKind - Type of join to use
|
|
54
|
+
* @param extraCondition - Additional join condition
|
|
55
|
+
* @returns Relation result with updated state and hydration
|
|
56
|
+
*/
|
|
57
|
+
joinRelation(
|
|
58
|
+
relationName: string,
|
|
59
|
+
joinKind: JoinKind,
|
|
60
|
+
extraCondition?: ExpressionNode
|
|
61
|
+
): RelationResult {
|
|
62
|
+
const nextState = this.withJoin(this.state, relationName, joinKind, extraCondition);
|
|
63
|
+
return { state: nextState, hydration: this.hydration };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Matches records based on a relation with an optional predicate
|
|
68
|
+
* @param relationName - Name of the relation to match
|
|
69
|
+
* @param predicate - Optional predicate expression
|
|
70
|
+
* @returns Relation result with updated state and hydration
|
|
71
|
+
*/
|
|
72
|
+
match(
|
|
73
|
+
relationName: string,
|
|
74
|
+
predicate?: ExpressionNode
|
|
75
|
+
): RelationResult {
|
|
76
|
+
const joined = this.joinRelation(relationName, JOIN_KINDS.INNER, predicate);
|
|
77
|
+
const pk = findPrimaryKey(this.table);
|
|
78
|
+
const distinctCols: ColumnNode[] = [{ type: 'Column', table: this.table.name, name: pk }];
|
|
79
|
+
const existingDistinct = joined.state.ast.distinct ? joined.state.ast.distinct : [];
|
|
80
|
+
const nextState = this.astService(joined.state).withDistinct([...existingDistinct, ...distinctCols]);
|
|
81
|
+
return { state: nextState, hydration: joined.hydration };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Includes a relation in the query result
|
|
86
|
+
* @param relationName - Name of the relation to include
|
|
87
|
+
* @param options - Options for relation inclusion
|
|
88
|
+
* @returns Relation result with updated state and hydration
|
|
89
|
+
*/
|
|
90
|
+
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
91
|
+
let state = this.state;
|
|
92
|
+
let hydration = this.hydration;
|
|
93
|
+
|
|
94
|
+
const relation = this.getRelation(relationName);
|
|
95
|
+
const aliasPrefix = options?.aliasPrefix ?? relationName;
|
|
96
|
+
const alreadyJoined = state.ast.joins.some(j => j.relationName === relationName);
|
|
97
|
+
|
|
98
|
+
if (!alreadyJoined) {
|
|
99
|
+
const joined = this.joinRelation(relationName, options?.joinKind ?? JOIN_KINDS.LEFT, options?.filter);
|
|
100
|
+
state = joined.state;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
|
|
104
|
+
state = projectionResult.state;
|
|
105
|
+
hydration = projectionResult.hydration;
|
|
106
|
+
|
|
107
|
+
const targetColumns = options?.columns?.length
|
|
108
|
+
? options.columns
|
|
109
|
+
: Object.keys(relation.target.columns);
|
|
110
|
+
|
|
111
|
+
const buildTypedSelection = (
|
|
112
|
+
columns: Record<string, ColumnDef>,
|
|
113
|
+
prefix: string,
|
|
114
|
+
keys: string[],
|
|
115
|
+
missingMsg: (col: string) => string
|
|
116
|
+
) : Record<string, ColumnDef> => {
|
|
117
|
+
return keys.reduce((acc, key) => {
|
|
118
|
+
const def = columns[key];
|
|
119
|
+
if (!def) {
|
|
120
|
+
throw new Error(missingMsg(key));
|
|
121
|
+
}
|
|
122
|
+
acc[makeRelationAlias(prefix, key)] = def;
|
|
123
|
+
return acc;
|
|
124
|
+
}, {} as Record<string, ColumnDef>);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const targetSelection = buildTypedSelection(
|
|
128
|
+
relation.target.columns as Record<string, ColumnDef>,
|
|
129
|
+
aliasPrefix,
|
|
130
|
+
targetColumns,
|
|
131
|
+
key => `Column '${key}' not found on relation '${relationName}'`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (relation.type !== RelationKinds.BelongsToMany) {
|
|
135
|
+
const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
|
|
136
|
+
state = relationSelectionResult.state;
|
|
137
|
+
hydration = relationSelectionResult.hydration;
|
|
138
|
+
|
|
139
|
+
hydration = hydration.onRelationIncluded(
|
|
140
|
+
state,
|
|
141
|
+
relation,
|
|
142
|
+
relationName,
|
|
143
|
+
aliasPrefix,
|
|
144
|
+
targetColumns
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return { state, hydration };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const many = relation as BelongsToManyRelation;
|
|
151
|
+
const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
|
|
152
|
+
const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
|
|
153
|
+
const pivotColumns =
|
|
154
|
+
options?.pivot?.columns ??
|
|
155
|
+
many.defaultPivotColumns ??
|
|
156
|
+
buildDefaultPivotColumns(many, pivotPk);
|
|
157
|
+
|
|
158
|
+
const pivotSelection = buildTypedSelection(
|
|
159
|
+
many.pivotTable.columns as Record<string, ColumnDef>,
|
|
160
|
+
pivotAliasPrefix,
|
|
161
|
+
pivotColumns,
|
|
162
|
+
key => `Column '${key}' not found on pivot table '${many.pivotTable.name}'`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const combinedSelection = {
|
|
166
|
+
...targetSelection,
|
|
167
|
+
...pivotSelection
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const relationSelectionResult = this.selectColumns(state, hydration, combinedSelection);
|
|
171
|
+
state = relationSelectionResult.state;
|
|
172
|
+
hydration = relationSelectionResult.hydration;
|
|
173
|
+
|
|
174
|
+
hydration = hydration.onRelationIncluded(
|
|
175
|
+
state,
|
|
176
|
+
relation,
|
|
177
|
+
relationName,
|
|
178
|
+
aliasPrefix,
|
|
179
|
+
targetColumns,
|
|
180
|
+
{ aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return { state, hydration };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Applies relation correlation to a query AST
|
|
188
|
+
* @param relationName - Name of the relation
|
|
189
|
+
* @param ast - Query AST to modify
|
|
190
|
+
* @returns Modified query AST with relation correlation
|
|
191
|
+
*/
|
|
192
|
+
applyRelationCorrelation(
|
|
193
|
+
relationName: string,
|
|
194
|
+
ast: SelectQueryNode
|
|
195
|
+
): SelectQueryNode {
|
|
196
|
+
const relation = this.getRelation(relationName);
|
|
197
|
+
const correlation = buildRelationCorrelation(this.table, relation);
|
|
198
|
+
const whereInSubquery = ast.where
|
|
199
|
+
? and(correlation, ast.where)
|
|
200
|
+
: correlation;
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
...ast,
|
|
204
|
+
where: whereInSubquery
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Creates a join node for a relation
|
|
210
|
+
* @param state - Current query state
|
|
211
|
+
* @param relationName - Name of the relation
|
|
212
|
+
* @param joinKind - Type of join to use
|
|
213
|
+
* @param extraCondition - Additional join condition
|
|
214
|
+
* @returns Updated query state with join
|
|
215
|
+
*/
|
|
216
|
+
private withJoin(
|
|
217
|
+
state: SelectQueryState,
|
|
218
|
+
relationName: string,
|
|
219
|
+
joinKind: JoinKind,
|
|
220
|
+
extraCondition?: ExpressionNode
|
|
221
|
+
): SelectQueryState {
|
|
222
|
+
const relation = this.getRelation(relationName);
|
|
223
|
+
if (relation.type === RelationKinds.BelongsToMany) {
|
|
224
|
+
const joins = buildBelongsToManyJoins(
|
|
225
|
+
this.table,
|
|
226
|
+
relationName,
|
|
227
|
+
relation as BelongsToManyRelation,
|
|
228
|
+
joinKind,
|
|
229
|
+
extraCondition
|
|
230
|
+
);
|
|
231
|
+
return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
|
|
235
|
+
const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
|
|
236
|
+
|
|
237
|
+
return this.astService(state).withJoin(joinNode);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Selects columns for a relation
|
|
242
|
+
* @param state - Current query state
|
|
243
|
+
* @param hydration - Hydration manager
|
|
244
|
+
* @param columns - Columns to select
|
|
245
|
+
* @returns Relation result with updated state and hydration
|
|
246
|
+
*/
|
|
247
|
+
private selectColumns(
|
|
248
|
+
state: SelectQueryState,
|
|
249
|
+
hydration: HydrationManager,
|
|
250
|
+
columns: Record<string, ColumnDef>
|
|
251
|
+
): RelationResult {
|
|
252
|
+
const { state: nextState, addedColumns } = this.astService(state).select(columns);
|
|
253
|
+
return {
|
|
254
|
+
state: nextState,
|
|
255
|
+
hydration: hydration.onColumnsSelected(nextState, addedColumns)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Gets a relation definition by name
|
|
261
|
+
* @param relationName - Name of the relation
|
|
262
|
+
* @returns Relation definition
|
|
263
|
+
* @throws Error if relation is not found
|
|
264
|
+
*/
|
|
265
|
+
private getRelation(relationName: string): RelationDef {
|
|
266
|
+
const relation = this.table.relations[relationName];
|
|
267
|
+
if (!relation) {
|
|
268
|
+
throw new Error(`Relation '${relationName}' not found on table '${this.table.name}'`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return relation;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Creates a QueryAstService instance
|
|
276
|
+
* @param state - Current query state
|
|
277
|
+
* @returns QueryAstService instance
|
|
278
|
+
*/
|
|
279
|
+
private astService(state: SelectQueryState = this.state): QueryAstService {
|
|
280
|
+
return this.createQueryAstService(this.table, state);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type { RelationResult } from './relation-projection-helper.js';
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { ExpressionNode } from '../ast/expression';
|
|
2
|
-
import { JOIN_KINDS } from '../
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Join kinds allowed when including a relation using `.include(...)`.
|
|
6
|
-
*/
|
|
7
|
-
export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Options for including a relation in a query
|
|
11
|
-
*/
|
|
12
|
-
export interface RelationIncludeOptions {
|
|
13
|
-
columns?: string[];
|
|
14
|
-
aliasPrefix?: string;
|
|
15
|
-
filter?: ExpressionNode;
|
|
16
|
-
joinKind?: RelationIncludeJoinKind;
|
|
17
|
-
pivot?: {
|
|
18
|
-
columns?: string[];
|
|
19
|
-
aliasPrefix?: string;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
1
|
+
import { ExpressionNode } from '../core/ast/expression.js';
|
|
2
|
+
import { JOIN_KINDS } from '../core/sql/sql.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Join kinds allowed when including a relation using `.include(...)`.
|
|
6
|
+
*/
|
|
7
|
+
export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for including a relation in a query
|
|
11
|
+
*/
|
|
12
|
+
export interface RelationIncludeOptions {
|
|
13
|
+
columns?: string[];
|
|
14
|
+
aliasPrefix?: string;
|
|
15
|
+
filter?: ExpressionNode;
|
|
16
|
+
joinKind?: RelationIncludeJoinKind;
|
|
17
|
+
pivot?: {
|
|
18
|
+
columns?: string[];
|
|
19
|
+
aliasPrefix?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { BelongsToManyRelation } from '../schema/relation';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Builds a default set of pivot columns, excluding keys used for joins.
|
|
5
|
-
*/
|
|
6
|
-
export const buildDefaultPivotColumns = (
|
|
7
|
-
rel: BelongsToManyRelation,
|
|
8
|
-
pivotPk: string
|
|
9
|
-
): string[] => {
|
|
10
|
-
const excluded = new Set([pivotPk, rel.pivotForeignKeyToRoot, rel.pivotForeignKeyToTarget]);
|
|
11
|
-
return Object.keys(rel.pivotTable.columns).filter(col => !excluded.has(col));
|
|
12
|
-
};
|
|
1
|
+
import { BelongsToManyRelation } from '../schema/relation.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds a default set of pivot columns, excluding keys used for joins.
|
|
5
|
+
*/
|
|
6
|
+
export const buildDefaultPivotColumns = (
|
|
7
|
+
rel: BelongsToManyRelation,
|
|
8
|
+
pivotPk: string
|
|
9
|
+
): string[] => {
|
|
10
|
+
const excluded = new Set([pivotPk, rel.pivotForeignKeyToRoot, rel.pivotForeignKeyToTarget]);
|
|
11
|
+
return Object.keys(rel.pivotTable.columns).filter(col => !excluded.has(col));
|
|
12
|
+
};
|