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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table';
|
|
2
2
|
import { ColumnDef } from '../schema/column';
|
|
3
|
-
import { RelationDef } from '../schema/relation';
|
|
3
|
+
import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation';
|
|
4
4
|
import { SelectQueryNode } from '../ast/query';
|
|
5
5
|
import {
|
|
6
6
|
ColumnNode,
|
|
@@ -13,11 +13,16 @@ import { QueryAstService } from './query-ast-service';
|
|
|
13
13
|
import { findPrimaryKey } from './hydration-planner';
|
|
14
14
|
import { RelationProjectionHelper } from './relation-projection-helper';
|
|
15
15
|
import type { RelationResult } from './relation-projection-helper';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
buildRelationJoinCondition,
|
|
18
|
+
buildRelationCorrelation,
|
|
19
|
+
buildBelongsToManyJoins
|
|
20
|
+
} from './relation-conditions';
|
|
17
21
|
import { JoinKind, JOIN_KINDS } from '../constants/sql';
|
|
18
|
-
import {
|
|
22
|
+
import { RelationIncludeOptions } from './relation-types';
|
|
19
23
|
import { createJoinNode } from '../utils/join-node';
|
|
20
24
|
import { makeRelationAlias } from '../utils/relation-alias';
|
|
25
|
+
import { buildDefaultPivotColumns } from './relation-utils';
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Service for handling relation operations (joins, includes, etc.)
|
|
@@ -82,10 +87,7 @@ export class RelationService {
|
|
|
82
87
|
* @param options - Options for relation inclusion
|
|
83
88
|
* @returns Relation result with updated state and hydration
|
|
84
89
|
*/
|
|
85
|
-
include(
|
|
86
|
-
relationName: string,
|
|
87
|
-
options?: { columns?: string[]; aliasPrefix?: string; filter?: ExpressionNode; joinKind?: RelationIncludeJoinKind }
|
|
88
|
-
): RelationResult {
|
|
90
|
+
include(relationName: string, options?: RelationIncludeOptions): RelationResult {
|
|
89
91
|
let state = this.state;
|
|
90
92
|
let hydration = this.hydration;
|
|
91
93
|
|
|
@@ -106,16 +108,66 @@ export class RelationService {
|
|
|
106
108
|
? options.columns
|
|
107
109
|
: Object.keys(relation.target.columns);
|
|
108
110
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return acc
|
|
116
|
-
|
|
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;
|
|
117
138
|
|
|
118
|
-
|
|
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);
|
|
119
171
|
state = relationSelectionResult.state;
|
|
120
172
|
hydration = relationSelectionResult.hydration;
|
|
121
173
|
|
|
@@ -124,7 +176,8 @@ export class RelationService {
|
|
|
124
176
|
relation,
|
|
125
177
|
relationName,
|
|
126
178
|
aliasPrefix,
|
|
127
|
-
targetColumns
|
|
179
|
+
targetColumns,
|
|
180
|
+
{ aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
|
|
128
181
|
);
|
|
129
182
|
|
|
130
183
|
return { state, hydration };
|
|
@@ -167,8 +220,18 @@ export class RelationService {
|
|
|
167
220
|
extraCondition?: ExpressionNode
|
|
168
221
|
): SelectQueryState {
|
|
169
222
|
const relation = this.getRelation(relationName);
|
|
170
|
-
|
|
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
|
+
}
|
|
171
233
|
|
|
234
|
+
const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
|
|
172
235
|
const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
|
|
173
236
|
|
|
174
237
|
return this.astService(state).withJoin(joinNode);
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
import { ExpressionNode } from '../ast/expression';
|
|
1
2
|
import { JOIN_KINDS } from '../constants/sql';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Join kinds allowed when including a relation using `.include(...)`.
|
|
5
6
|
*/
|
|
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
|
+
}
|
|
@@ -0,0 +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
|
+
};
|
package/src/builder/select.ts
CHANGED
|
@@ -26,7 +26,8 @@ import { CteManager } from './operations/cte-manager';
|
|
|
26
26
|
import { JoinManager } from './operations/join-manager';
|
|
27
27
|
import { FilterManager } from './operations/filter-manager';
|
|
28
28
|
import { PaginationManager } from './operations/pagination-manager';
|
|
29
|
-
import { RelationManager
|
|
29
|
+
import { RelationManager } from './operations/relation-manager';
|
|
30
|
+
import { RelationIncludeOptions } from './relation-types';
|
|
30
31
|
import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../constants/sql';
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { ColumnNode, ExpressionNode, valueToOperand } from '../ast/expression';
|
|
3
|
+
import { TableNode, UpdateQueryNode, UpdateAssignmentNode } from '../ast/query';
|
|
4
|
+
|
|
5
|
+
const createTableNode = (table: TableDef): TableNode => ({
|
|
6
|
+
type: 'Table',
|
|
7
|
+
name: table.name
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Immutable state for UPDATE queries
|
|
12
|
+
*/
|
|
13
|
+
export class UpdateQueryState {
|
|
14
|
+
public readonly table: TableDef;
|
|
15
|
+
public readonly ast: UpdateQueryNode;
|
|
16
|
+
|
|
17
|
+
constructor(table: TableDef, ast?: UpdateQueryNode) {
|
|
18
|
+
this.table = table;
|
|
19
|
+
this.ast = ast ?? {
|
|
20
|
+
type: 'UpdateQuery',
|
|
21
|
+
table: createTableNode(table),
|
|
22
|
+
set: []
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private clone(nextAst: UpdateQueryNode): UpdateQueryState {
|
|
27
|
+
return new UpdateQueryState(this.table, nextAst);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
withSet(values: Record<string, unknown>): UpdateQueryState {
|
|
31
|
+
const assignments: UpdateAssignmentNode[] = Object.entries(values).map(([column, value]) => ({
|
|
32
|
+
column: {
|
|
33
|
+
type: 'Column',
|
|
34
|
+
table: this.table.name,
|
|
35
|
+
name: column
|
|
36
|
+
},
|
|
37
|
+
value: valueToOperand(value)
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return this.clone({
|
|
41
|
+
...this.ast,
|
|
42
|
+
set: assignments
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
withWhere(expr: ExpressionNode): UpdateQueryState {
|
|
47
|
+
return this.clone({
|
|
48
|
+
...this.ast,
|
|
49
|
+
where: expr
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
withReturning(columns: ColumnNode[]): UpdateQueryState {
|
|
54
|
+
return this.clone({
|
|
55
|
+
...this.ast,
|
|
56
|
+
returning: [...columns]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table';
|
|
2
|
+
import { ColumnDef } from '../schema/column';
|
|
3
|
+
import { ColumnNode, ExpressionNode } from '../ast/expression';
|
|
4
|
+
import { CompiledQuery, UpdateCompiler } from '../dialect/abstract';
|
|
5
|
+
import { UpdateQueryNode } from '../ast/query';
|
|
6
|
+
import { UpdateQueryState } from './update-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 UPDATE queries
|
|
22
|
+
*/
|
|
23
|
+
export class UpdateQueryBuilder<T> {
|
|
24
|
+
private readonly table: TableDef;
|
|
25
|
+
private readonly state: UpdateQueryState;
|
|
26
|
+
|
|
27
|
+
constructor(table: TableDef, state?: UpdateQueryState) {
|
|
28
|
+
this.table = table;
|
|
29
|
+
this.state = state ?? new UpdateQueryState(table);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private clone(state: UpdateQueryState): UpdateQueryBuilder<T> {
|
|
33
|
+
return new UpdateQueryBuilder(this.table, state);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set(values: Record<string, unknown>): UpdateQueryBuilder<T> {
|
|
37
|
+
return this.clone(this.state.withSet(values));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
where(expr: ExpressionNode): UpdateQueryBuilder<T> {
|
|
41
|
+
return this.clone(this.state.withWhere(expr));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
returning(...columns: (ColumnDef | ColumnNode)[]): UpdateQueryBuilder<T> {
|
|
45
|
+
if (!columns.length) return this;
|
|
46
|
+
const nodes = columns.map(column => buildColumnNode(this.table, column));
|
|
47
|
+
return this.clone(this.state.withReturning(nodes));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
compile(compiler: UpdateCompiler): CompiledQuery {
|
|
51
|
+
return compiler.compileUpdate(this.state.ast);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toSql(compiler: UpdateCompiler): string {
|
|
55
|
+
return this.compile(compiler).sql;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getAST(): UpdateQueryNode {
|
|
59
|
+
return this.state.ast;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/dialect/abstract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SelectQueryNode } from '../ast/query';
|
|
1
|
+
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../ast/query';
|
|
2
2
|
import {
|
|
3
3
|
ExpressionNode,
|
|
4
4
|
BinaryExpressionNode,
|
|
@@ -30,39 +30,91 @@ export interface CompilerContext {
|
|
|
30
30
|
/**
|
|
31
31
|
* Result of SQL compilation
|
|
32
32
|
*/
|
|
33
|
-
export interface CompiledQuery {
|
|
34
|
-
/** Generated SQL string */
|
|
35
|
-
sql: string;
|
|
36
|
-
/** Parameters for the query */
|
|
37
|
-
params: unknown[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
export interface CompiledQuery {
|
|
34
|
+
/** Generated SQL string */
|
|
35
|
+
sql: string;
|
|
36
|
+
/** Parameters for the query */
|
|
37
|
+
params: unknown[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SelectCompiler {
|
|
41
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface InsertCompiler {
|
|
45
|
+
compileInsert(ast: InsertQueryNode): CompiledQuery;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface UpdateCompiler {
|
|
49
|
+
compileUpdate(ast: UpdateQueryNode): CompiledQuery;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DeleteCompiler {
|
|
53
|
+
compileDelete(ast: DeleteQueryNode): CompiledQuery;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Abstract base class for SQL dialect implementations
|
|
58
|
+
*/
|
|
59
|
+
export abstract class Dialect
|
|
60
|
+
implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler
|
|
61
|
+
{
|
|
44
62
|
/**
|
|
45
63
|
* Compiles a SELECT query AST to SQL
|
|
46
64
|
* @param ast - Query AST to compile
|
|
47
65
|
* @returns Compiled query with SQL and parameters
|
|
48
66
|
*/
|
|
49
|
-
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
50
|
-
const ctx = this.createCompilerContext();
|
|
51
|
-
const rawSql = this.compileSelectAst(ast, ctx).trim();
|
|
52
|
-
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
53
|
-
return {
|
|
54
|
-
sql,
|
|
55
|
-
params: [...ctx.params]
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
68
|
+
const ctx = this.createCompilerContext();
|
|
69
|
+
const rawSql = this.compileSelectAst(ast, ctx).trim();
|
|
70
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
71
|
+
return {
|
|
72
|
+
sql,
|
|
73
|
+
params: [...ctx.params]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
78
|
+
const ctx = this.createCompilerContext();
|
|
79
|
+
const rawSql = this.compileInsertAst(ast, ctx).trim();
|
|
80
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
81
|
+
return {
|
|
82
|
+
sql,
|
|
83
|
+
params: [...ctx.params]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
compileUpdate(ast: UpdateQueryNode): CompiledQuery {
|
|
88
|
+
const ctx = this.createCompilerContext();
|
|
89
|
+
const rawSql = this.compileUpdateAst(ast, ctx).trim();
|
|
90
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
91
|
+
return {
|
|
92
|
+
sql,
|
|
93
|
+
params: [...ctx.params]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
compileDelete(ast: DeleteQueryNode): CompiledQuery {
|
|
98
|
+
const ctx = this.createCompilerContext();
|
|
99
|
+
const rawSql = this.compileDeleteAst(ast, ctx).trim();
|
|
100
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
101
|
+
return {
|
|
102
|
+
sql,
|
|
103
|
+
params: [...ctx.params]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
|
|
109
|
+
* @param ast - Query AST
|
|
110
|
+
* @param ctx - Compiler context
|
|
111
|
+
* @returns SQL string
|
|
112
|
+
*/
|
|
113
|
+
protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
|
|
114
|
+
|
|
115
|
+
protected abstract compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string;
|
|
116
|
+
protected abstract compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string;
|
|
117
|
+
protected abstract compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string;
|
|
66
118
|
|
|
67
119
|
/**
|
|
68
120
|
* Quotes an SQL identifier (to be implemented by concrete dialects)
|
|
@@ -77,10 +129,18 @@ export abstract class Dialect {
|
|
|
77
129
|
* @param ctx - Compiler context
|
|
78
130
|
* @returns SQL WHERE clause or empty string
|
|
79
131
|
*/
|
|
80
|
-
protected compileWhere(where: ExpressionNode | undefined, ctx: CompilerContext): string {
|
|
81
|
-
if (!where) return '';
|
|
82
|
-
return ` WHERE ${this.compileExpression(where, ctx)}`;
|
|
83
|
-
}
|
|
132
|
+
protected compileWhere(where: ExpressionNode | undefined, ctx: CompilerContext): string {
|
|
133
|
+
if (!where) return '';
|
|
134
|
+
return ` WHERE ${this.compileExpression(where, ctx)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected compileReturning(
|
|
138
|
+
returning: ColumnNode[] | undefined,
|
|
139
|
+
ctx: CompilerContext
|
|
140
|
+
): string {
|
|
141
|
+
if (!returning || returning.length === 0) return '';
|
|
142
|
+
throw new Error('RETURNING is not supported by this dialect.');
|
|
143
|
+
}
|
|
84
144
|
|
|
85
145
|
/**
|
|
86
146
|
* Generates subquery for EXISTS expressions
|
|
@@ -163,13 +223,13 @@ export abstract class Dialect {
|
|
|
163
223
|
* @param ctx - Compiler context
|
|
164
224
|
* @returns Compiled SQL expression
|
|
165
225
|
*/
|
|
166
|
-
protected compileExpression(node: ExpressionNode, ctx: CompilerContext): string {
|
|
167
|
-
const compiler = this.expressionCompilers.get(node.type);
|
|
168
|
-
if (!compiler) {
|
|
169
|
-
throw new Error(`Unsupported expression node type "${node.type}" for ${this.constructor.name}`);
|
|
170
|
-
}
|
|
171
|
-
return compiler(node, ctx);
|
|
172
|
-
}
|
|
226
|
+
protected compileExpression(node: ExpressionNode, ctx: CompilerContext): string {
|
|
227
|
+
const compiler = this.expressionCompilers.get(node.type);
|
|
228
|
+
if (!compiler) {
|
|
229
|
+
throw new Error(`Unsupported expression node type "${node.type}" for ${this.constructor.name}`);
|
|
230
|
+
}
|
|
231
|
+
return compiler(node, ctx);
|
|
232
|
+
}
|
|
173
233
|
|
|
174
234
|
/**
|
|
175
235
|
* Compiles an operand node
|
|
@@ -177,13 +237,13 @@ export abstract class Dialect {
|
|
|
177
237
|
* @param ctx - Compiler context
|
|
178
238
|
* @returns Compiled SQL operand
|
|
179
239
|
*/
|
|
180
|
-
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
181
|
-
const compiler = this.operandCompilers.get(node.type);
|
|
182
|
-
if (!compiler) {
|
|
183
|
-
throw new Error(`Unsupported operand node type "${node.type}" for ${this.constructor.name}`);
|
|
184
|
-
}
|
|
185
|
-
return compiler(node, ctx);
|
|
186
|
-
}
|
|
240
|
+
protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
241
|
+
const compiler = this.operandCompilers.get(node.type);
|
|
242
|
+
if (!compiler) {
|
|
243
|
+
throw new Error(`Unsupported operand node type "${node.type}" for ${this.constructor.name}`);
|
|
244
|
+
}
|
|
245
|
+
return compiler(node, ctx);
|
|
246
|
+
}
|
|
187
247
|
|
|
188
248
|
private registerDefaultExpressionCompilers(): void {
|
|
189
249
|
this.registerExpressionCompiler('BinaryExpression', (binary: BinaryExpressionNode, ctx) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CompilerContext, Dialect } from '../abstract';
|
|
2
|
-
import { SelectQueryNode } from '../../ast/query';
|
|
3
|
-
import { JsonPathNode } from '../../ast/expression';
|
|
1
|
+
import { CompilerContext, Dialect } from '../abstract';
|
|
2
|
+
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query';
|
|
3
|
+
import { JsonPathNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Microsoft SQL Server dialect implementation
|
|
@@ -48,7 +48,7 @@ export class SqlServerDialect extends Dialect {
|
|
|
48
48
|
* @param ctx - Compiler context
|
|
49
49
|
* @returns SQL Server SQL string
|
|
50
50
|
*/
|
|
51
|
-
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
51
|
+
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
52
52
|
const columns = ast.columns.map(c => {
|
|
53
53
|
let expr = '';
|
|
54
54
|
if (c.type === 'Function') {
|
|
@@ -111,6 +111,31 @@ export class SqlServerDialect extends Dialect {
|
|
|
111
111
|
}).join(', ') + ' '
|
|
112
112
|
: '';
|
|
113
113
|
|
|
114
|
-
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy};`;
|
|
115
|
-
}
|
|
114
|
+
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy};`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
118
|
+
const table = this.quoteIdentifier(ast.into.name);
|
|
119
|
+
const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
|
|
120
|
+
const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
|
|
121
|
+
return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
|
|
125
|
+
const table = this.quoteIdentifier(ast.table.name);
|
|
126
|
+
const assignments = ast.set.map(assignment => {
|
|
127
|
+
const col = assignment.column;
|
|
128
|
+
const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
|
|
129
|
+
const value = this.compileOperand(assignment.value, ctx);
|
|
130
|
+
return `${target} = ${value}`;
|
|
131
|
+
}).join(', ');
|
|
132
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
133
|
+
return `UPDATE ${table} SET ${assignments}${whereClause};`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
137
|
+
const table = this.quoteIdentifier(ast.from.name);
|
|
138
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
139
|
+
return `DELETE FROM ${table}${whereClause};`;
|
|
140
|
+
}
|
|
116
141
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CompilerContext, Dialect } from '../abstract';
|
|
2
|
-
import { SelectQueryNode } from '../../ast/query';
|
|
3
|
-
import { JsonPathNode } from '../../ast/expression';
|
|
1
|
+
import { CompilerContext, Dialect } from '../abstract';
|
|
2
|
+
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query';
|
|
3
|
+
import { JsonPathNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* MySQL dialect implementation
|
|
@@ -39,7 +39,7 @@ export class MySqlDialect extends Dialect {
|
|
|
39
39
|
* @param ctx - Compiler context
|
|
40
40
|
* @returns MySQL SQL string
|
|
41
41
|
*/
|
|
42
|
-
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
42
|
+
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
43
43
|
const columns = ast.columns.map(c => {
|
|
44
44
|
let expr = '';
|
|
45
45
|
if (c.type === 'Function') {
|
|
@@ -98,6 +98,31 @@ export class MySqlDialect extends Dialect {
|
|
|
98
98
|
})()
|
|
99
99
|
: '';
|
|
100
100
|
|
|
101
|
-
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
|
|
102
|
-
}
|
|
101
|
+
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
105
|
+
const table = this.quoteIdentifier(ast.into.name);
|
|
106
|
+
const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
|
|
107
|
+
const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
|
|
108
|
+
return `INSERT INTO ${table} (${columnList}) VALUES ${values};`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
|
|
112
|
+
const table = this.quoteIdentifier(ast.table.name);
|
|
113
|
+
const assignments = ast.set.map(assignment => {
|
|
114
|
+
const col = assignment.column;
|
|
115
|
+
const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
|
|
116
|
+
const value = this.compileOperand(assignment.value, ctx);
|
|
117
|
+
return `${target} = ${value}`;
|
|
118
|
+
}).join(', ');
|
|
119
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
120
|
+
return `UPDATE ${table} SET ${assignments}${whereClause};`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
124
|
+
const table = this.quoteIdentifier(ast.from.name);
|
|
125
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
126
|
+
return `DELETE FROM ${table}${whereClause};`;
|
|
127
|
+
}
|
|
103
128
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CompilerContext, Dialect } from '../abstract';
|
|
2
|
-
import { SelectQueryNode } from '../../ast/query';
|
|
3
|
-
import { JsonPathNode } from '../../ast/expression';
|
|
1
|
+
import { CompilerContext, Dialect } from '../abstract';
|
|
2
|
+
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query';
|
|
3
|
+
import { JsonPathNode, ColumnNode } from '../../ast/expression';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* PostgreSQL dialect implementation
|
|
@@ -39,7 +39,7 @@ export class PostgresDialect extends Dialect {
|
|
|
39
39
|
* @param ctx - Compiler context
|
|
40
40
|
* @returns PostgreSQL SQL string
|
|
41
41
|
*/
|
|
42
|
-
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
42
|
+
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
43
43
|
const columns = ast.columns.map(c => {
|
|
44
44
|
let expr = '';
|
|
45
45
|
if (c.type === 'Function') {
|
|
@@ -98,6 +98,45 @@ export class PostgresDialect extends Dialect {
|
|
|
98
98
|
})()
|
|
99
99
|
: '';
|
|
100
100
|
|
|
101
|
-
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
|
|
102
|
-
}
|
|
101
|
+
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
105
|
+
const table = this.quoteIdentifier(ast.into.name);
|
|
106
|
+
const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
|
|
107
|
+
const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
|
|
108
|
+
const returning = this.compileReturning(ast.returning, ctx);
|
|
109
|
+
return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning};`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
|
|
113
|
+
const table = this.quoteIdentifier(ast.table.name);
|
|
114
|
+
const assignments = ast.set.map(assignment => {
|
|
115
|
+
const col = assignment.column;
|
|
116
|
+
const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
|
|
117
|
+
const value = this.compileOperand(assignment.value, ctx);
|
|
118
|
+
return `${target} = ${value}`;
|
|
119
|
+
}).join(', ');
|
|
120
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
121
|
+
const returning = this.compileReturning(ast.returning, ctx);
|
|
122
|
+
return `UPDATE ${table} SET ${assignments}${whereClause}${returning};`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
126
|
+
const table = this.quoteIdentifier(ast.from.name);
|
|
127
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
128
|
+
const returning = this.compileReturning(ast.returning, ctx);
|
|
129
|
+
return `DELETE FROM ${table}${whereClause}${returning};`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
|
|
133
|
+
if (!returning || returning.length === 0) return '';
|
|
134
|
+
const columns = returning
|
|
135
|
+
.map(column => {
|
|
136
|
+
const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
|
|
137
|
+
return `${tablePart}${this.quoteIdentifier(column.name)}`;
|
|
138
|
+
})
|
|
139
|
+
.join(', ');
|
|
140
|
+
return ` RETURNING ${columns}`;
|
|
141
|
+
}
|
|
103
142
|
}
|