metal-orm 1.0.3 → 1.0.5
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/docs/hydration.md +10 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +632 -614
- 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 +89 -33
- 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 +2 -1
- package/src/builder/update-query-state.ts +59 -0
- package/src/builder/update.ts +61 -0
- 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 +6 -3
- package/src/playground/features/playground/data/scenarios/hydration.ts +23 -11
- package/src/playground/features/playground/data/schema.ts +10 -6
- package/src/runtime/hydration.ts +17 -5
- package/src/schema/relation.ts +59 -18
- package/tests/belongs-to-many.test.ts +57 -0
- package/tests/dml.test.ts +206 -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
|
|
@@ -62,15 +63,21 @@ export class HydrationPlanner {
|
|
|
62
63
|
* @param columns - Columns to include from the relation
|
|
63
64
|
* @returns Updated HydrationPlanner with included relation
|
|
64
65
|
*/
|
|
65
|
-
includeRelation(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
83
|
* Gets the current hydration plan
|
|
@@ -96,31 +103,80 @@ export class HydrationPlanner {
|
|
|
96
103
|
* @param columns - Columns to include from the relation
|
|
97
104
|
* @returns Hydration relation plan
|
|
98
105
|
*/
|
|
99
|
-
private buildRelationPlan(
|
|
100
|
-
|
|
101
|
-
|
|
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);
|
|
102
150
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
170
|
}
|
|
115
171
|
|
|
116
|
-
/**
|
|
117
|
-
* Builds a default hydration plan for a table
|
|
118
|
-
* @param table - Table definition
|
|
119
|
-
* @returns Default hydration plan
|
|
120
|
-
*/
|
|
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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { ColumnDef } from '../schema/column';
|
|
3
|
+
import { ColumnNode } from '../ast/expression';
|
|
4
|
+
import { CompiledQuery, InsertCompiler } from '../dialect/abstract';
|
|
5
|
+
import { InsertQueryNode } from '../ast/query';
|
|
6
|
+
import { InsertQueryState } from './insert-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 INSERT queries
|
|
22
|
+
*/
|
|
23
|
+
export class InsertQueryBuilder<T> {
|
|
24
|
+
private readonly table: TableDef;
|
|
25
|
+
private readonly state: InsertQueryState;
|
|
26
|
+
|
|
27
|
+
constructor(table: TableDef, state?: InsertQueryState) {
|
|
28
|
+
this.table = table;
|
|
29
|
+
this.state = state ?? new InsertQueryState(table);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private clone(state: InsertQueryState): InsertQueryBuilder<T> {
|
|
33
|
+
return new InsertQueryBuilder(this.table, state);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
values(rowOrRows: Record<string, unknown> | Record<string, unknown>[]): InsertQueryBuilder<T> {
|
|
37
|
+
const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows];
|
|
38
|
+
if (!rows.length) return this;
|
|
39
|
+
return this.clone(this.state.withValues(rows));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
returning(...columns: (ColumnDef | ColumnNode)[]): InsertQueryBuilder<T> {
|
|
43
|
+
if (!columns.length) return this;
|
|
44
|
+
const nodes = columns.map(column => buildColumnNode(this.table, column));
|
|
45
|
+
return this.clone(this.state.withReturning(nodes));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
compile(compiler: InsertCompiler): CompiledQuery {
|
|
49
|
+
return compiler.compileInsert(this.state.ast);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toSql(compiler: InsertCompiler): string {
|
|
53
|
+
return this.compile(compiler).sql;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getAST(): InsertQueryNode {
|
|
57
|
+
return this.state.ast;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -2,29 +2,7 @@ import { ExpressionNode } from '../../ast/expression';
|
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
|
|
4
4
|
import { JoinKind } from '../../constants/sql';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Options for including relations in queries
|
|
9
|
-
*/
|
|
10
|
-
export interface RelationIncludeOptions {
|
|
11
|
-
/**
|
|
12
|
-
* Columns to include from the related table
|
|
13
|
-
*/
|
|
14
|
-
columns?: string[];
|
|
15
|
-
/**
|
|
16
|
-
* Alias prefix for the relation columns
|
|
17
|
-
*/
|
|
18
|
-
aliasPrefix?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Filter expression to apply to the relation
|
|
21
|
-
*/
|
|
22
|
-
filter?: ExpressionNode;
|
|
23
|
-
/**
|
|
24
|
-
* Type of join to use for the relation
|
|
25
|
-
*/
|
|
26
|
-
joinKind?: RelationIncludeJoinKind;
|
|
27
|
-
}
|
|
5
|
+
import { RelationIncludeOptions } from '../relation-types';
|
|
28
6
|
|
|
29
7
|
/**
|
|
30
8
|
* Manages relation operations (joins, includes, etc.) for query building
|
|
@@ -1,7 +1,10 @@
|
|
|
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 { ExpressionNode, eq, and } from '../ast/expression';
|
|
4
4
|
import { findPrimaryKey } from './hydration-planner';
|
|
5
|
+
import { JoinNode } from '../ast/join';
|
|
6
|
+
import { JoinKind } from '../constants/sql';
|
|
7
|
+
import { createJoinNode } from '../utils/join-node';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Utility function to handle unreachable code paths
|
|
@@ -36,11 +39,52 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef): Expressio
|
|
|
36
39
|
{ type: 'Column', table: relation.target.name, name: localKey },
|
|
37
40
|
{ type: 'Column', table: root.name, name: relation.foreignKey }
|
|
38
41
|
);
|
|
42
|
+
case RelationKinds.BelongsToMany:
|
|
43
|
+
throw new Error('BelongsToMany relations do not support the standard join condition builder');
|
|
39
44
|
default:
|
|
40
45
|
return assertNever(relation);
|
|
41
46
|
}
|
|
42
47
|
};
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Builds the join nodes required to include a BelongsToMany relation.
|
|
51
|
+
*/
|
|
52
|
+
export const buildBelongsToManyJoins = (
|
|
53
|
+
root: TableDef,
|
|
54
|
+
relationName: string,
|
|
55
|
+
relation: BelongsToManyRelation,
|
|
56
|
+
joinKind: JoinKind,
|
|
57
|
+
extra?: ExpressionNode
|
|
58
|
+
): JoinNode[] => {
|
|
59
|
+
const rootKey = relation.localKey || findPrimaryKey(root);
|
|
60
|
+
const targetKey = relation.targetKey || findPrimaryKey(relation.target);
|
|
61
|
+
|
|
62
|
+
const pivotCondition = eq(
|
|
63
|
+
{ type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
|
|
64
|
+
{ type: 'Column', table: root.name, name: rootKey }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const pivotJoin = createJoinNode(joinKind, relation.pivotTable.name, pivotCondition);
|
|
68
|
+
|
|
69
|
+
let targetCondition: ExpressionNode = eq(
|
|
70
|
+
{ type: 'Column', table: relation.target.name, name: targetKey },
|
|
71
|
+
{ type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (extra) {
|
|
75
|
+
targetCondition = and(targetCondition, extra);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const targetJoin = createJoinNode(
|
|
79
|
+
joinKind,
|
|
80
|
+
relation.target.name,
|
|
81
|
+
targetCondition,
|
|
82
|
+
relationName
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return [pivotJoin, targetJoin];
|
|
86
|
+
};
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* Builds a relation join condition with optional extra conditions
|
|
46
90
|
* @param root - Root table definition
|