metal-orm 1.0.4 → 1.0.6
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 +299 -113
- package/docs/CHANGES.md +104 -0
- package/docs/advanced-features.md +92 -1
- package/docs/api-reference.md +13 -4
- package/docs/dml-operations.md +156 -0
- package/docs/getting-started.md +122 -55
- package/docs/hydration.md +77 -3
- package/docs/index.md +19 -14
- package/docs/multi-dialect-support.md +25 -0
- package/docs/query-builder.md +60 -0
- package/docs/runtime.md +105 -0
- package/docs/schema-definition.md +52 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +630 -592
- package/src/ast/query.ts +110 -49
- package/src/builder/delete-query-state.ts +42 -0
- package/src/builder/delete.ts +57 -0
- package/src/builder/hydration-manager.ts +3 -2
- package/src/builder/hydration-planner.ts +163 -107
- package/src/builder/insert-query-state.ts +62 -0
- package/src/builder/insert.ts +59 -0
- package/src/builder/operations/relation-manager.ts +1 -23
- package/src/builder/relation-conditions.ts +45 -1
- package/src/builder/relation-service.ts +81 -18
- package/src/builder/relation-types.ts +15 -0
- package/src/builder/relation-utils.ts +12 -0
- package/src/builder/select.ts +427 -394
- package/src/builder/update-query-state.ts +59 -0
- package/src/builder/update.ts +61 -0
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/dialect/abstract.ts +107 -47
- package/src/dialect/mssql/index.ts +31 -6
- package/src/dialect/mysql/index.ts +31 -6
- package/src/dialect/postgres/index.ts +45 -6
- package/src/dialect/sqlite/index.ts +45 -6
- package/src/index.ts +22 -11
- package/src/playground/features/playground/data/scenarios/hydration.ts +23 -11
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +6 -2
- package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
- package/src/runtime/entity-meta.ts +52 -0
- package/src/runtime/entity.ts +252 -0
- package/src/runtime/execute.ts +36 -0
- package/src/runtime/hydration.ts +100 -38
- package/src/runtime/lazy-batch.ts +205 -0
- package/src/runtime/orm-context.ts +539 -0
- package/src/runtime/relations/belongs-to.ts +92 -0
- package/src/runtime/relations/has-many.ts +111 -0
- package/src/runtime/relations/many-to-many.ts +149 -0
- package/src/schema/column.ts +15 -1
- package/src/schema/relation.ts +105 -40
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/belongs-to-many.test.ts +57 -0
- package/tests/dml.test.ts +206 -0
- package/tests/orm-runtime.test.ts +254 -0
package/src/ast/query.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ColumnNode,
|
|
3
|
+
FunctionNode,
|
|
4
|
+
ExpressionNode,
|
|
5
|
+
ScalarSubqueryNode,
|
|
6
|
+
CaseExpressionNode,
|
|
7
|
+
WindowFunctionNode,
|
|
8
|
+
OperandNode
|
|
9
|
+
} from './expression';
|
|
2
10
|
import { JoinNode } from './join';
|
|
3
11
|
import { RelationType } from '../schema/relation';
|
|
4
12
|
import { OrderDirection } from '../constants/sql';
|
|
@@ -27,27 +35,39 @@ export interface OrderByNode {
|
|
|
27
35
|
direction: OrderDirection;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
/**
|
|
31
|
-
* Plan
|
|
32
|
-
*/
|
|
33
|
-
export interface
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Plan describing pivot columns needed for hydration
|
|
40
|
+
*/
|
|
41
|
+
export interface HydrationPivotPlan {
|
|
42
|
+
table: string;
|
|
43
|
+
primaryKey: string;
|
|
44
|
+
aliasPrefix: string;
|
|
45
|
+
columns: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Plan for hydrating relationship data
|
|
50
|
+
*/
|
|
51
|
+
export interface HydrationRelationPlan {
|
|
52
|
+
/** Name of the relationship */
|
|
53
|
+
name: string;
|
|
54
|
+
/** Alias prefix for the relationship */
|
|
55
|
+
aliasPrefix: string;
|
|
56
|
+
/** Type of relationship */
|
|
57
|
+
type: RelationType;
|
|
58
|
+
/** Target table name */
|
|
59
|
+
targetTable: string;
|
|
60
|
+
/** Target table primary key */
|
|
61
|
+
targetPrimaryKey: string;
|
|
62
|
+
/** Foreign key column */
|
|
63
|
+
foreignKey: string;
|
|
64
|
+
/** Local key column */
|
|
65
|
+
localKey: string;
|
|
66
|
+
/** Columns to include */
|
|
67
|
+
columns: string[];
|
|
68
|
+
/** Optional pivot plan for many-to-many relationships */
|
|
69
|
+
pivot?: HydrationPivotPlan;
|
|
70
|
+
}
|
|
51
71
|
|
|
52
72
|
/**
|
|
53
73
|
* Complete hydration plan for a query
|
|
@@ -89,30 +109,71 @@ export interface CommonTableExpressionNode {
|
|
|
89
109
|
/**
|
|
90
110
|
* AST node representing a complete SELECT query
|
|
91
111
|
*/
|
|
92
|
-
export interface SelectQueryNode {
|
|
93
|
-
type: 'SelectQuery';
|
|
94
|
-
/** Optional CTEs (WITH clauses) */
|
|
95
|
-
ctes?: CommonTableExpressionNode[];
|
|
96
|
-
/** FROM clause table */
|
|
97
|
-
from: TableNode;
|
|
98
|
-
/** SELECT clause columns */
|
|
99
|
-
columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
|
|
100
|
-
/** JOIN clauses */
|
|
101
|
-
joins: JoinNode[];
|
|
102
|
-
/** Optional WHERE clause */
|
|
103
|
-
where?: ExpressionNode;
|
|
104
|
-
/** Optional GROUP BY clause */
|
|
105
|
-
groupBy?: ColumnNode[];
|
|
106
|
-
/** Optional HAVING clause */
|
|
107
|
-
having?: ExpressionNode;
|
|
108
|
-
/** Optional ORDER BY clause */
|
|
109
|
-
orderBy?: OrderByNode[];
|
|
110
|
-
/** Optional LIMIT clause */
|
|
111
|
-
limit?: number;
|
|
112
|
-
/** Optional OFFSET clause */
|
|
113
|
-
offset?: number;
|
|
114
|
-
/** Optional query metadata */
|
|
115
|
-
meta?: QueryMetadata;
|
|
116
|
-
/** Optional DISTINCT clause */
|
|
117
|
-
distinct?: ColumnNode[];
|
|
118
|
-
}
|
|
112
|
+
export interface SelectQueryNode {
|
|
113
|
+
type: 'SelectQuery';
|
|
114
|
+
/** Optional CTEs (WITH clauses) */
|
|
115
|
+
ctes?: CommonTableExpressionNode[];
|
|
116
|
+
/** FROM clause table */
|
|
117
|
+
from: TableNode;
|
|
118
|
+
/** SELECT clause columns */
|
|
119
|
+
columns: (ColumnNode | FunctionNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode)[];
|
|
120
|
+
/** JOIN clauses */
|
|
121
|
+
joins: JoinNode[];
|
|
122
|
+
/** Optional WHERE clause */
|
|
123
|
+
where?: ExpressionNode;
|
|
124
|
+
/** Optional GROUP BY clause */
|
|
125
|
+
groupBy?: ColumnNode[];
|
|
126
|
+
/** Optional HAVING clause */
|
|
127
|
+
having?: ExpressionNode;
|
|
128
|
+
/** Optional ORDER BY clause */
|
|
129
|
+
orderBy?: OrderByNode[];
|
|
130
|
+
/** Optional LIMIT clause */
|
|
131
|
+
limit?: number;
|
|
132
|
+
/** Optional OFFSET clause */
|
|
133
|
+
offset?: number;
|
|
134
|
+
/** Optional query metadata */
|
|
135
|
+
meta?: QueryMetadata;
|
|
136
|
+
/** Optional DISTINCT clause */
|
|
137
|
+
distinct?: ColumnNode[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface InsertQueryNode {
|
|
141
|
+
type: 'InsertQuery';
|
|
142
|
+
/** Target table */
|
|
143
|
+
into: TableNode;
|
|
144
|
+
/** Column order for inserted values */
|
|
145
|
+
columns: ColumnNode[];
|
|
146
|
+
/** Rows of values to insert */
|
|
147
|
+
values: OperandNode[][];
|
|
148
|
+
/** Optional RETURNING clause */
|
|
149
|
+
returning?: ColumnNode[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface UpdateAssignmentNode {
|
|
153
|
+
/** Column to update */
|
|
154
|
+
column: ColumnNode;
|
|
155
|
+
/** Value to set */
|
|
156
|
+
value: OperandNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface UpdateQueryNode {
|
|
160
|
+
type: 'UpdateQuery';
|
|
161
|
+
/** Table being updated */
|
|
162
|
+
table: TableNode;
|
|
163
|
+
/** Assignments for SET clause */
|
|
164
|
+
set: UpdateAssignmentNode[];
|
|
165
|
+
/** Optional WHERE clause */
|
|
166
|
+
where?: ExpressionNode;
|
|
167
|
+
/** Optional RETURNING clause */
|
|
168
|
+
returning?: ColumnNode[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface DeleteQueryNode {
|
|
172
|
+
type: 'DeleteQuery';
|
|
173
|
+
/** Table to delete from */
|
|
174
|
+
from: TableNode;
|
|
175
|
+
/** Optional WHERE clause */
|
|
176
|
+
where?: ExpressionNode;
|
|
177
|
+
/** Optional RETURNING clause */
|
|
178
|
+
returning?: ColumnNode[];
|
|
179
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { ColumnNode, ExpressionNode } from '../ast/expression';
|
|
3
|
+
import { TableNode, DeleteQueryNode } from '../ast/query';
|
|
4
|
+
|
|
5
|
+
const createTableNode = (table: TableDef): TableNode => ({
|
|
6
|
+
type: 'Table',
|
|
7
|
+
name: table.name
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maintains immutable state for DELETE queries
|
|
12
|
+
*/
|
|
13
|
+
export class DeleteQueryState {
|
|
14
|
+
public readonly table: TableDef;
|
|
15
|
+
public readonly ast: DeleteQueryNode;
|
|
16
|
+
|
|
17
|
+
constructor(table: TableDef, ast?: DeleteQueryNode) {
|
|
18
|
+
this.table = table;
|
|
19
|
+
this.ast = ast ?? {
|
|
20
|
+
type: 'DeleteQuery',
|
|
21
|
+
from: createTableNode(table)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private clone(nextAst: DeleteQueryNode): DeleteQueryState {
|
|
26
|
+
return new DeleteQueryState(this.table, nextAst);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
withWhere(expr: ExpressionNode): DeleteQueryState {
|
|
30
|
+
return this.clone({
|
|
31
|
+
...this.ast,
|
|
32
|
+
where: expr
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
withReturning(columns: ColumnNode[]): DeleteQueryState {
|
|
37
|
+
return this.clone({
|
|
38
|
+
...this.ast,
|
|
39
|
+
returning: [...columns]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { ColumnDef } from '../schema/column';
|
|
3
|
+
import { ColumnNode, ExpressionNode } from '../ast/expression';
|
|
4
|
+
import { CompiledQuery, DeleteCompiler } from '../dialect/abstract';
|
|
5
|
+
import { DeleteQueryNode } from '../ast/query';
|
|
6
|
+
import { DeleteQueryState } from './delete-query-state';
|
|
7
|
+
|
|
8
|
+
const buildColumnNode = (table: TableDef, column: ColumnDef | ColumnNode): ColumnNode => {
|
|
9
|
+
if ((column as ColumnNode).type === 'Column') {
|
|
10
|
+
return column as ColumnNode;
|
|
11
|
+
}
|
|
12
|
+
const def = column as ColumnDef;
|
|
13
|
+
return {
|
|
14
|
+
type: 'Column',
|
|
15
|
+
table: def.table || table.name,
|
|
16
|
+
name: def.name
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builder for DELETE queries
|
|
22
|
+
*/
|
|
23
|
+
export class DeleteQueryBuilder<T> {
|
|
24
|
+
private readonly table: TableDef;
|
|
25
|
+
private readonly state: DeleteQueryState;
|
|
26
|
+
|
|
27
|
+
constructor(table: TableDef, state?: DeleteQueryState) {
|
|
28
|
+
this.table = table;
|
|
29
|
+
this.state = state ?? new DeleteQueryState(table);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private clone(state: DeleteQueryState): DeleteQueryBuilder<T> {
|
|
33
|
+
return new DeleteQueryBuilder(this.table, state);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
where(expr: ExpressionNode): DeleteQueryBuilder<T> {
|
|
37
|
+
return this.clone(this.state.withWhere(expr));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
returning(...columns: (ColumnDef | ColumnNode)[]): DeleteQueryBuilder<T> {
|
|
41
|
+
if (!columns.length) return this;
|
|
42
|
+
const nodes = columns.map(column => buildColumnNode(this.table, column));
|
|
43
|
+
return this.clone(this.state.withReturning(nodes));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
compile(compiler: DeleteCompiler): CompiledQuery {
|
|
47
|
+
return compiler.compileDelete(this.state.ast);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
toSql(compiler: DeleteCompiler): string {
|
|
51
|
+
return this.compile(compiler).sql;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getAST(): DeleteQueryNode {
|
|
55
|
+
return this.state.ast;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -52,10 +52,11 @@ export class HydrationManager {
|
|
|
52
52
|
relation: RelationDef,
|
|
53
53
|
relationName: string,
|
|
54
54
|
aliasPrefix: string,
|
|
55
|
-
targetColumns: string[]
|
|
55
|
+
targetColumns: string[],
|
|
56
|
+
pivot?: { aliasPrefix: string; columns: string[] }
|
|
56
57
|
): HydrationManager {
|
|
57
58
|
const withRoots = this.planner.captureRootColumns(state.ast.columns);
|
|
58
|
-
const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns);
|
|
59
|
+
const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns, pivot);
|
|
59
60
|
return this.clone(next);
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table';
|
|
2
|
-
import { RelationDef, RelationKinds } from '../schema/relation';
|
|
2
|
+
import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
|
|
3
3
|
import { ProjectionNode } from './select-query-state';
|
|
4
4
|
import { HydrationPlan, HydrationRelationPlan } from '../ast/query';
|
|
5
5
|
import { isRelationAlias } from '../utils/relation-alias';
|
|
6
|
+
import { buildDefaultPivotColumns } from './relation-utils';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Finds the primary key column name for a table
|
|
@@ -13,114 +14,169 @@ export const findPrimaryKey = (table: TableDef): string => {
|
|
|
13
14
|
const pk = Object.values(table.columns).find(c => c.primary);
|
|
14
15
|
return pk?.name || 'id';
|
|
15
16
|
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Manages hydration planning for query results
|
|
19
|
-
*/
|
|
20
|
-
export class HydrationPlanner {
|
|
21
|
-
/**
|
|
22
|
-
* Creates a new HydrationPlanner instance
|
|
23
|
-
* @param table - Table definition
|
|
24
|
-
* @param plan - Optional existing hydration plan
|
|
25
|
-
*/
|
|
26
|
-
constructor(private readonly table: TableDef, private readonly plan?: HydrationPlan) { }
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Captures root table columns for hydration planning
|
|
30
|
-
* @param columns - Columns to capture
|
|
31
|
-
* @returns Updated HydrationPlanner with captured columns
|
|
32
|
-
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Manages hydration planning for query results
|
|
20
|
+
*/
|
|
21
|
+
export class HydrationPlanner {
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new HydrationPlanner instance
|
|
24
|
+
* @param table - Table definition
|
|
25
|
+
* @param plan - Optional existing hydration plan
|
|
26
|
+
*/
|
|
27
|
+
constructor(private readonly table: TableDef, private readonly plan?: HydrationPlan) { }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Captures root table columns for hydration planning
|
|
31
|
+
* @param columns - Columns to capture
|
|
32
|
+
* @returns Updated HydrationPlanner with captured columns
|
|
33
|
+
*/
|
|
33
34
|
captureRootColumns(columns: ProjectionNode[]): HydrationPlanner {
|
|
34
35
|
const currentPlan = this.getPlanOrDefault();
|
|
35
|
-
const rootCols = new Set(currentPlan.rootColumns);
|
|
36
|
-
let changed = false;
|
|
37
|
-
|
|
38
|
-
columns.forEach(node => {
|
|
39
|
-
if (node.type !== 'Column') return;
|
|
40
|
-
if (node.table !== this.table.name) return;
|
|
41
|
-
|
|
42
|
-
const alias = node.alias || node.name;
|
|
43
|
-
if (isRelationAlias(alias)) return;
|
|
44
|
-
if (!rootCols.has(alias)) {
|
|
45
|
-
rootCols.add(alias);
|
|
46
|
-
changed = true;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!changed) return this;
|
|
51
|
-
return new HydrationPlanner(this.table, {
|
|
52
|
-
...currentPlan,
|
|
53
|
-
rootColumns: Array.from(rootCols)
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Includes a relation in the hydration plan
|
|
59
|
-
* @param rel - Relation definition
|
|
60
|
-
* @param relationName - Name of the relation
|
|
61
|
-
* @param aliasPrefix - Alias prefix for relation columns
|
|
62
|
-
* @param columns - Columns to include from the relation
|
|
63
|
-
* @returns Updated HydrationPlanner with included relation
|
|
64
|
-
*/
|
|
65
|
-
includeRelation(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* @param columns - Columns to include from the relation
|
|
97
|
-
* @returns Hydration relation plan
|
|
98
|
-
*/
|
|
99
|
-
private buildRelationPlan(rel: RelationDef, relationName: string, aliasPrefix: string, columns: string[]): HydrationRelationPlan {
|
|
100
|
-
const localKeyFallback =
|
|
101
|
-
rel.type === RelationKinds.HasMany ? findPrimaryKey(this.table) : findPrimaryKey(rel.target);
|
|
36
|
+
const rootCols = new Set(currentPlan.rootColumns);
|
|
37
|
+
let changed = false;
|
|
38
|
+
|
|
39
|
+
columns.forEach(node => {
|
|
40
|
+
if (node.type !== 'Column') return;
|
|
41
|
+
if (node.table !== this.table.name) return;
|
|
42
|
+
|
|
43
|
+
const alias = node.alias || node.name;
|
|
44
|
+
if (isRelationAlias(alias)) return;
|
|
45
|
+
if (!rootCols.has(alias)) {
|
|
46
|
+
rootCols.add(alias);
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!changed) return this;
|
|
52
|
+
return new HydrationPlanner(this.table, {
|
|
53
|
+
...currentPlan,
|
|
54
|
+
rootColumns: Array.from(rootCols)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Includes a relation in the hydration plan
|
|
60
|
+
* @param rel - Relation definition
|
|
61
|
+
* @param relationName - Name of the relation
|
|
62
|
+
* @param aliasPrefix - Alias prefix for relation columns
|
|
63
|
+
* @param columns - Columns to include from the relation
|
|
64
|
+
* @returns Updated HydrationPlanner with included relation
|
|
65
|
+
*/
|
|
66
|
+
includeRelation(
|
|
67
|
+
rel: RelationDef,
|
|
68
|
+
relationName: string,
|
|
69
|
+
aliasPrefix: string,
|
|
70
|
+
columns: string[],
|
|
71
|
+
pivot?: { aliasPrefix: string; columns: string[] }
|
|
72
|
+
): HydrationPlanner {
|
|
73
|
+
const currentPlan = this.getPlanOrDefault();
|
|
74
|
+
const relations = currentPlan.relations.filter(r => r.name !== relationName);
|
|
75
|
+
relations.push(this.buildRelationPlan(rel, relationName, aliasPrefix, columns, pivot));
|
|
76
|
+
return new HydrationPlanner(this.table, {
|
|
77
|
+
...currentPlan,
|
|
78
|
+
relations
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets the current hydration plan
|
|
84
|
+
* @returns Current hydration plan or undefined
|
|
85
|
+
*/
|
|
86
|
+
getPlan(): HydrationPlan | undefined {
|
|
87
|
+
return this.plan;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gets the current hydration plan or creates a default one
|
|
92
|
+
* @returns Current hydration plan or default plan
|
|
93
|
+
*/
|
|
94
|
+
private getPlanOrDefault(): HydrationPlan {
|
|
95
|
+
return this.plan ?? buildDefaultHydrationPlan(this.table);
|
|
96
|
+
}
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Builds a relation plan for hydration
|
|
100
|
+
* @param rel - Relation definition
|
|
101
|
+
* @param relationName - Name of the relation
|
|
102
|
+
* @param aliasPrefix - Alias prefix for relation columns
|
|
103
|
+
* @param columns - Columns to include from the relation
|
|
104
|
+
* @returns Hydration relation plan
|
|
105
|
+
*/
|
|
106
|
+
private buildRelationPlan(
|
|
107
|
+
rel: RelationDef,
|
|
108
|
+
relationName: string,
|
|
109
|
+
aliasPrefix: string,
|
|
110
|
+
columns: string[],
|
|
111
|
+
pivot?: { aliasPrefix: string; columns: string[] }
|
|
112
|
+
): HydrationRelationPlan {
|
|
113
|
+
switch (rel.type) {
|
|
114
|
+
case RelationKinds.HasMany: {
|
|
115
|
+
const localKey = rel.localKey || findPrimaryKey(this.table);
|
|
116
|
+
return {
|
|
117
|
+
name: relationName,
|
|
118
|
+
aliasPrefix,
|
|
119
|
+
type: rel.type,
|
|
120
|
+
targetTable: rel.target.name,
|
|
121
|
+
targetPrimaryKey: findPrimaryKey(rel.target),
|
|
122
|
+
foreignKey: rel.foreignKey,
|
|
123
|
+
localKey,
|
|
124
|
+
columns
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case RelationKinds.BelongsTo: {
|
|
128
|
+
const localKey = rel.localKey || findPrimaryKey(rel.target);
|
|
129
|
+
return {
|
|
130
|
+
name: relationName,
|
|
131
|
+
aliasPrefix,
|
|
132
|
+
type: rel.type,
|
|
133
|
+
targetTable: rel.target.name,
|
|
134
|
+
targetPrimaryKey: findPrimaryKey(rel.target),
|
|
135
|
+
foreignKey: rel.foreignKey,
|
|
136
|
+
localKey,
|
|
137
|
+
columns
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case RelationKinds.BelongsToMany: {
|
|
141
|
+
const many = rel as BelongsToManyRelation;
|
|
142
|
+
const localKey = many.localKey || findPrimaryKey(this.table);
|
|
143
|
+
const targetPk = many.targetKey || findPrimaryKey(many.target);
|
|
144
|
+
const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
|
|
145
|
+
const pivotAliasPrefix = pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
|
|
146
|
+
const pivotColumns =
|
|
147
|
+
pivot?.columns ??
|
|
148
|
+
many.defaultPivotColumns ??
|
|
149
|
+
buildDefaultPivotColumns(many, pivotPk);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name: relationName,
|
|
153
|
+
aliasPrefix,
|
|
154
|
+
type: rel.type,
|
|
155
|
+
targetTable: many.target.name,
|
|
156
|
+
targetPrimaryKey: targetPk,
|
|
157
|
+
foreignKey: many.pivotForeignKeyToRoot,
|
|
158
|
+
localKey,
|
|
159
|
+
columns,
|
|
160
|
+
pivot: {
|
|
161
|
+
table: many.pivotTable.name,
|
|
162
|
+
primaryKey: pivotPk,
|
|
163
|
+
aliasPrefix: pivotAliasPrefix,
|
|
164
|
+
columns: pivotColumns
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
113
169
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Builds a default hydration plan for a table
|
|
118
|
-
* @param table - Table definition
|
|
119
|
-
* @returns Default hydration plan
|
|
120
|
-
*/
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Builds a default hydration plan for a table
|
|
174
|
+
* @param table - Table definition
|
|
175
|
+
* @returns Default hydration plan
|
|
176
|
+
*/
|
|
121
177
|
const buildDefaultHydrationPlan = (table: TableDef): HydrationPlan => ({
|
|
122
|
-
rootTable: table.name,
|
|
123
|
-
rootPrimaryKey: findPrimaryKey(table),
|
|
124
|
-
rootColumns: [],
|
|
125
|
-
relations: []
|
|
126
|
-
});
|
|
178
|
+
rootTable: table.name,
|
|
179
|
+
rootPrimaryKey: findPrimaryKey(table),
|
|
180
|
+
rootColumns: [],
|
|
181
|
+
relations: []
|
|
182
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { InsertQueryNode, TableNode } from '../ast/query';
|
|
3
|
+
import { ColumnNode, OperandNode, valueToOperand } from '../ast/expression';
|
|
4
|
+
|
|
5
|
+
const createTableNode = (table: TableDef): TableNode => ({
|
|
6
|
+
type: 'Table',
|
|
7
|
+
name: table.name
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const buildColumnNodes = (table: TableDef, names: string[]): ColumnNode[] =>
|
|
11
|
+
names.map(name => ({
|
|
12
|
+
type: 'Column',
|
|
13
|
+
table: table.name,
|
|
14
|
+
name
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maintains immutable state for building INSERT queries
|
|
19
|
+
*/
|
|
20
|
+
export class InsertQueryState {
|
|
21
|
+
public readonly table: TableDef;
|
|
22
|
+
public readonly ast: InsertQueryNode;
|
|
23
|
+
|
|
24
|
+
constructor(table: TableDef, ast?: InsertQueryNode) {
|
|
25
|
+
this.table = table;
|
|
26
|
+
this.ast = ast ?? {
|
|
27
|
+
type: 'InsertQuery',
|
|
28
|
+
into: createTableNode(table),
|
|
29
|
+
columns: [],
|
|
30
|
+
values: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private clone(nextAst: InsertQueryNode): InsertQueryState {
|
|
35
|
+
return new InsertQueryState(this.table, nextAst);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
withValues(rows: Record<string, unknown>[]): InsertQueryState {
|
|
39
|
+
if (!rows.length) return this;
|
|
40
|
+
|
|
41
|
+
const definedColumns = this.ast.columns.length
|
|
42
|
+
? this.ast.columns
|
|
43
|
+
: buildColumnNodes(this.table, Object.keys(rows[0]));
|
|
44
|
+
|
|
45
|
+
const newRows: OperandNode[][] = rows.map(row =>
|
|
46
|
+
definedColumns.map(column => valueToOperand(row[column.name]))
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return this.clone({
|
|
50
|
+
...this.ast,
|
|
51
|
+
columns: definedColumns,
|
|
52
|
+
values: [...this.ast.values, ...newRows]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
withReturning(columns: ColumnNode[]): InsertQueryState {
|
|
57
|
+
return this.clone({
|
|
58
|
+
...this.ast,
|
|
59
|
+
returning: [...columns]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|