metal-orm 1.0.12 → 1.0.14
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 +20 -20
- package/dist/decorators/index.cjs +302 -32
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -1
- package/dist/decorators/index.d.ts +1 -1
- package/dist/decorators/index.js +302 -32
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +583 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +583 -130
- package/dist/index.js.map +1 -1
- package/dist/{select-BKlr2ivY.d.cts → select-CCp1oz9p.d.cts} +114 -1
- package/dist/{select-BKlr2ivY.d.ts → select-CCp1oz9p.d.ts} +114 -1
- package/package.json +3 -2
- package/src/core/ast/query.ts +40 -22
- package/src/core/dialect/abstract.ts +122 -37
- package/src/core/dialect/base/sql-dialect.ts +51 -8
- package/src/core/dialect/mssql/index.ts +125 -80
- package/src/core/dialect/postgres/index.ts +5 -6
- package/src/core/dialect/sqlite/index.ts +5 -6
- package/src/orm/execute.ts +25 -16
- package/src/orm/orm-context.ts +60 -55
- package/src/orm/query-logger.ts +38 -0
- package/src/orm/relations/belongs-to.ts +42 -26
- package/src/orm/relations/has-many.ts +41 -25
- package/src/orm/relations/many-to-many.ts +43 -27
- package/src/orm/unit-of-work.ts +60 -23
- package/src/query-builder/hydration-manager.ts +229 -25
- package/src/query-builder/query-ast-service.ts +27 -12
- package/src/query-builder/select-query-state.ts +24 -12
- package/src/query-builder/select.ts +58 -14
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
SelectQueryNode,
|
|
3
|
+
InsertQueryNode,
|
|
4
|
+
UpdateQueryNode,
|
|
5
|
+
DeleteQueryNode,
|
|
6
|
+
SetOperationKind,
|
|
7
|
+
CommonTableExpressionNode
|
|
8
|
+
} from '../ast/query.js';
|
|
2
9
|
import {
|
|
3
10
|
ExpressionNode,
|
|
4
11
|
BinaryExpressionNode,
|
|
@@ -56,22 +63,23 @@ export interface DeleteCompiler {
|
|
|
56
63
|
/**
|
|
57
64
|
* Abstract base class for SQL dialect implementations
|
|
58
65
|
*/
|
|
59
|
-
export abstract class Dialect
|
|
60
|
-
implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler
|
|
61
|
-
{
|
|
66
|
+
export abstract class Dialect
|
|
67
|
+
implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler
|
|
68
|
+
{
|
|
62
69
|
/**
|
|
63
70
|
* Compiles a SELECT query AST to SQL
|
|
64
71
|
* @param ast - Query AST to compile
|
|
65
72
|
* @returns Compiled query with SQL and parameters
|
|
66
73
|
*/
|
|
67
|
-
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
68
|
-
const ctx = this.createCompilerContext();
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
75
|
+
const ctx = this.createCompilerContext();
|
|
76
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
77
|
+
const rawSql = this.compileSelectAst(normalized, ctx).trim();
|
|
78
|
+
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
79
|
+
return {
|
|
80
|
+
sql,
|
|
81
|
+
params: [...ctx.params]
|
|
82
|
+
};
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
@@ -94,15 +102,19 @@ export abstract class Dialect
|
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
compileDelete(ast: DeleteQueryNode): CompiledQuery {
|
|
98
|
-
const ctx = this.createCompilerContext();
|
|
99
|
-
const rawSql = this.compileDeleteAst(ast, ctx).trim();
|
|
105
|
+
compileDelete(ast: DeleteQueryNode): CompiledQuery {
|
|
106
|
+
const ctx = this.createCompilerContext();
|
|
107
|
+
const rawSql = this.compileDeleteAst(ast, ctx).trim();
|
|
100
108
|
const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
|
|
101
109
|
return {
|
|
102
110
|
sql,
|
|
103
111
|
params: [...ctx.params]
|
|
104
|
-
};
|
|
105
|
-
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
supportsReturning(): boolean {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
106
118
|
|
|
107
119
|
/**
|
|
108
120
|
* Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
|
|
@@ -110,7 +122,7 @@ export abstract class Dialect
|
|
|
110
122
|
* @param ctx - Compiler context
|
|
111
123
|
* @returns SQL string
|
|
112
124
|
*/
|
|
113
|
-
protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
|
|
125
|
+
protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
|
|
114
126
|
|
|
115
127
|
protected abstract compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string;
|
|
116
128
|
protected abstract compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string;
|
|
@@ -149,16 +161,23 @@ export abstract class Dialect
|
|
|
149
161
|
* Does not add ';' at the end
|
|
150
162
|
* @param ast - Query AST
|
|
151
163
|
* @param ctx - Compiler context
|
|
152
|
-
* @returns SQL for EXISTS subquery
|
|
153
|
-
*/
|
|
154
|
-
protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
* @returns SQL for EXISTS subquery
|
|
165
|
+
*/
|
|
166
|
+
protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
167
|
+
const normalized = this.normalizeSelectAst(ast);
|
|
168
|
+
const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, '');
|
|
169
|
+
|
|
170
|
+
// When the subquery is a set operation, wrap it as a derived table to keep valid syntax.
|
|
171
|
+
if (normalized.setOps && normalized.setOps.length > 0) {
|
|
172
|
+
return `SELECT 1 FROM (${full}) AS _exists`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const upper = full.toUpperCase();
|
|
176
|
+
const fromIndex = upper.indexOf(' FROM ');
|
|
177
|
+
if (fromIndex === -1) {
|
|
178
|
+
return full;
|
|
179
|
+
}
|
|
180
|
+
|
|
162
181
|
const tail = full.slice(fromIndex);
|
|
163
182
|
return `SELECT 1${tail}`;
|
|
164
183
|
}
|
|
@@ -185,15 +204,81 @@ export abstract class Dialect
|
|
|
185
204
|
* @param index - Parameter index
|
|
186
205
|
* @returns Formatted placeholder string
|
|
187
206
|
*/
|
|
188
|
-
protected formatPlaceholder(index: number): string {
|
|
189
|
-
return '?';
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
protected formatPlaceholder(index: number): string {
|
|
208
|
+
return '?';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Whether the current dialect supports a given set operation.
|
|
213
|
+
* Override in concrete dialects to restrict support.
|
|
214
|
+
*/
|
|
215
|
+
protected supportsSetOperation(kind: SetOperationKind): boolean {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validates set-operation semantics:
|
|
221
|
+
* - Ensures the dialect supports requested operators.
|
|
222
|
+
* - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
|
|
223
|
+
* @param ast - Query to validate
|
|
224
|
+
* @param isOutermost - Whether this node is the outermost compound query
|
|
225
|
+
*/
|
|
226
|
+
protected validateSetOperations(ast: SelectQueryNode, isOutermost = true): void {
|
|
227
|
+
const hasSetOps = !!(ast.setOps && ast.setOps.length);
|
|
228
|
+
if (!isOutermost && (ast.orderBy || ast.limit !== undefined || ast.offset !== undefined)) {
|
|
229
|
+
throw new Error('ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (hasSetOps) {
|
|
233
|
+
for (const op of ast.setOps!) {
|
|
234
|
+
if (!this.supportsSetOperation(op.operator)) {
|
|
235
|
+
throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
|
|
236
|
+
}
|
|
237
|
+
this.validateSetOperations(op.query, false);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
|
|
244
|
+
* @param ast - Query AST
|
|
245
|
+
* @returns Normalized AST without inner CTEs and a list of hoisted CTEs
|
|
246
|
+
*/
|
|
247
|
+
private hoistCtes(ast: SelectQueryNode): { normalized: SelectQueryNode; hoistedCtes: CommonTableExpressionNode[] } {
|
|
248
|
+
let hoisted: CommonTableExpressionNode[] = [];
|
|
249
|
+
|
|
250
|
+
const normalizedSetOps = ast.setOps?.map(op => {
|
|
251
|
+
const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
|
|
252
|
+
const childCtes = child.ctes ?? [];
|
|
253
|
+
if (childCtes.length) {
|
|
254
|
+
hoisted = hoisted.concat(childCtes);
|
|
255
|
+
}
|
|
256
|
+
hoisted = hoisted.concat(childHoisted);
|
|
257
|
+
const queryWithoutCtes = childCtes.length ? { ...child, ctes: undefined } : child;
|
|
258
|
+
return { ...op, query: queryWithoutCtes };
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const normalized: SelectQueryNode = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
|
|
262
|
+
return { normalized, hoistedCtes: hoisted };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Normalizes a SELECT AST before compilation (validation + CTE hoisting).
|
|
267
|
+
* @param ast - Query AST
|
|
268
|
+
* @returns Normalized query AST
|
|
269
|
+
*/
|
|
270
|
+
protected normalizeSelectAst(ast: SelectQueryNode): SelectQueryNode {
|
|
271
|
+
this.validateSetOperations(ast, true);
|
|
272
|
+
const { normalized, hoistedCtes } = this.hoistCtes(ast);
|
|
273
|
+
const combinedCtes = [...(normalized.ctes ?? []), ...hoistedCtes];
|
|
274
|
+
return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private readonly expressionCompilers: Map<string, (node: ExpressionNode, ctx: CompilerContext) => string>;
|
|
278
|
+
private readonly operandCompilers: Map<string, (node: OperandNode, ctx: CompilerContext) => string>;
|
|
279
|
+
|
|
280
|
+
protected constructor() {
|
|
281
|
+
this.expressionCompilers = new Map();
|
|
197
282
|
this.operandCompilers = new Map();
|
|
198
283
|
this.registerDefaultOperandCompilers();
|
|
199
284
|
this.registerDefaultExpressionCompilers();
|
|
@@ -18,17 +18,29 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
18
18
|
* Compiles SELECT query AST to SQL using common rules.
|
|
19
19
|
*/
|
|
20
20
|
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
21
|
+
const hasSetOps = !!(ast.setOps && ast.setOps.length);
|
|
21
22
|
const ctes = this.compileCtes(ast, ctx);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
|
|
24
|
+
// When set operations exist, omit ORDER BY/OFFSET/LIMIT from the operands and apply at the end.
|
|
25
|
+
const baseAst: SelectQueryNode = hasSetOps
|
|
26
|
+
? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
|
|
27
|
+
: ast;
|
|
28
|
+
|
|
29
|
+
const baseSelect = this.compileSelectCore(baseAst, ctx);
|
|
30
|
+
|
|
31
|
+
if (!hasSetOps) {
|
|
32
|
+
return `${ctes}${baseSelect}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const compound = ast.setOps!
|
|
36
|
+
.map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
|
|
37
|
+
.join(' ');
|
|
38
|
+
|
|
28
39
|
const orderBy = this.compileOrderBy(ast);
|
|
29
40
|
const pagination = this.compilePagination(ast, orderBy);
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
|
|
43
|
+
return `${ctes}${combined}${orderBy}${pagination}`;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
@@ -41,6 +53,22 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
41
53
|
return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning}`;
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Compiles a single SELECT (no set operations, no CTE prefix).
|
|
58
|
+
*/
|
|
59
|
+
private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
60
|
+
const columns = this.compileSelectColumns(ast, ctx);
|
|
61
|
+
const from = this.compileFrom(ast.from);
|
|
62
|
+
const joins = this.compileJoins(ast, ctx);
|
|
63
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
64
|
+
const groupBy = this.compileGroupBy(ast);
|
|
65
|
+
const having = this.compileHaving(ast, ctx);
|
|
66
|
+
const orderBy = this.compileOrderBy(ast);
|
|
67
|
+
const pagination = this.compilePagination(ast, orderBy);
|
|
68
|
+
|
|
69
|
+
return `SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
|
|
45
73
|
const table = this.compileTableName(ast.table);
|
|
46
74
|
const assignments = ast.set.map(assignment => {
|
|
@@ -69,6 +97,16 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
69
97
|
throw new Error('RETURNING is not supported by this dialect.');
|
|
70
98
|
}
|
|
71
99
|
|
|
100
|
+
protected formatReturningColumns(returning: ColumnNode[]): string {
|
|
101
|
+
return returning
|
|
102
|
+
.map(column => {
|
|
103
|
+
const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
|
|
104
|
+
const aliasPart = column.alias ? ` AS ${this.quoteIdentifier(column.alias)}` : '';
|
|
105
|
+
return `${tablePart}${this.quoteIdentifier(column.name)}${aliasPart}`;
|
|
106
|
+
})
|
|
107
|
+
.join(', ');
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
/**
|
|
73
111
|
* DISTINCT clause. Override for DISTINCT ON support.
|
|
74
112
|
*/
|
|
@@ -149,7 +187,7 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
149
187
|
const cols = cte.columns && cte.columns.length
|
|
150
188
|
? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})`
|
|
151
189
|
: '';
|
|
152
|
-
const query = this.stripTrailingSemicolon(this.compileSelectAst(cte.query, ctx));
|
|
190
|
+
const query = this.stripTrailingSemicolon(this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx));
|
|
153
191
|
return `${name}${cols} AS (${query})`;
|
|
154
192
|
}).join(', ');
|
|
155
193
|
return `${prefix}${cteDefs} `;
|
|
@@ -158,4 +196,9 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
158
196
|
protected stripTrailingSemicolon(sql: string): string {
|
|
159
197
|
return sql.trim().replace(/;$/, '');
|
|
160
198
|
}
|
|
199
|
+
|
|
200
|
+
protected wrapSetOperand(sql: string): string {
|
|
201
|
+
const trimmed = this.stripTrailingSemicolon(sql);
|
|
202
|
+
return `(${trimmed})`;
|
|
203
|
+
}
|
|
161
204
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CompilerContext, Dialect } from '../abstract.js';
|
|
2
|
-
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query.js';
|
|
3
|
-
import { JsonPathNode } from '../../ast/expression.js';
|
|
1
|
+
import { CompilerContext, Dialect } from '../abstract.js';
|
|
2
|
+
import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query.js';
|
|
3
|
+
import { JsonPathNode } from '../../ast/expression.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Microsoft SQL Server dialect implementation
|
|
@@ -42,77 +42,36 @@ export class SqlServerDialect extends Dialect {
|
|
|
42
42
|
return `@p${index}`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Compiles SELECT query AST to SQL Server SQL
|
|
47
|
-
* @param ast - Query AST
|
|
48
|
-
* @param ctx - Compiler context
|
|
49
|
-
* @returns SQL Server SQL string
|
|
50
|
-
*/
|
|
51
|
-
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const table = this.quoteIdentifier(j.table.name);
|
|
76
|
-
const cond = this.compileExpression(j.condition, ctx);
|
|
77
|
-
return `${j.kind} JOIN ${table} ON ${cond}`;
|
|
78
|
-
}).join(' ');
|
|
79
|
-
const whereClause = this.compileWhere(ast.where, ctx);
|
|
80
|
-
|
|
81
|
-
const groupBy = ast.groupBy && ast.groupBy.length > 0
|
|
82
|
-
? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
|
|
83
|
-
: '';
|
|
84
|
-
|
|
85
|
-
const having = ast.having
|
|
86
|
-
? ` HAVING ${this.compileExpression(ast.having, ctx)}`
|
|
87
|
-
: '';
|
|
88
|
-
|
|
89
|
-
const orderBy = ast.orderBy && ast.orderBy.length > 0
|
|
90
|
-
? ' ORDER BY ' + ast.orderBy.map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(', ')
|
|
91
|
-
: '';
|
|
92
|
-
|
|
93
|
-
let pagination = '';
|
|
94
|
-
if (ast.limit || ast.offset) {
|
|
95
|
-
const off = ast.offset || 0;
|
|
96
|
-
const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
|
|
97
|
-
pagination = `${orderClause} OFFSET ${off} ROWS`;
|
|
98
|
-
if (ast.limit) {
|
|
99
|
-
pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
|
|
100
|
-
}
|
|
101
|
-
return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination};`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const ctes = ast.ctes && ast.ctes.length > 0
|
|
105
|
-
? 'WITH ' + ast.ctes.map(cte => {
|
|
106
|
-
// MSSQL does not use RECURSIVE keyword
|
|
107
|
-
const name = this.quoteIdentifier(cte.name);
|
|
108
|
-
const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
|
|
109
|
-
const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, '');
|
|
110
|
-
return `${name}${cols} AS (${query})`;
|
|
111
|
-
}).join(', ') + ' '
|
|
112
|
-
: '';
|
|
113
|
-
|
|
114
|
-
return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy};`;
|
|
115
|
-
}
|
|
45
|
+
/**
|
|
46
|
+
* Compiles SELECT query AST to SQL Server SQL
|
|
47
|
+
* @param ast - Query AST
|
|
48
|
+
* @param ctx - Compiler context
|
|
49
|
+
* @returns SQL Server SQL string
|
|
50
|
+
*/
|
|
51
|
+
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
52
|
+
const hasSetOps = !!(ast.setOps && ast.setOps.length);
|
|
53
|
+
const ctes = this.compileCtes(ast, ctx);
|
|
54
|
+
|
|
55
|
+
const baseAst: SelectQueryNode = hasSetOps
|
|
56
|
+
? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
|
|
57
|
+
: ast;
|
|
58
|
+
|
|
59
|
+
const baseSelect = this.compileSelectCore(baseAst, ctx);
|
|
60
|
+
|
|
61
|
+
if (!hasSetOps) {
|
|
62
|
+
return `${ctes}${baseSelect}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const compound = ast.setOps!
|
|
66
|
+
.map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
|
|
67
|
+
.join(' ');
|
|
68
|
+
|
|
69
|
+
const orderBy = this.compileOrderBy(ast);
|
|
70
|
+
const pagination = this.compilePagination(ast, orderBy);
|
|
71
|
+
const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
|
|
72
|
+
const tail = pagination || orderBy;
|
|
73
|
+
return `${ctes}${combined}${tail}`;
|
|
74
|
+
}
|
|
116
75
|
|
|
117
76
|
protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
118
77
|
const table = this.quoteIdentifier(ast.into.name);
|
|
@@ -133,9 +92,95 @@ export class SqlServerDialect extends Dialect {
|
|
|
133
92
|
return `UPDATE ${table} SET ${assignments}${whereClause};`;
|
|
134
93
|
}
|
|
135
94
|
|
|
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
|
-
}
|
|
141
|
-
|
|
95
|
+
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
96
|
+
const table = this.quoteIdentifier(ast.from.name);
|
|
97
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
98
|
+
return `DELETE FROM ${table}${whereClause};`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
102
|
+
const columns = ast.columns.map(c => {
|
|
103
|
+
let expr = '';
|
|
104
|
+
if (c.type === 'Function') {
|
|
105
|
+
expr = this.compileOperand(c, ctx);
|
|
106
|
+
} else if (c.type === 'Column') {
|
|
107
|
+
expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
|
|
108
|
+
} else if (c.type === 'ScalarSubquery') {
|
|
109
|
+
expr = this.compileOperand(c, ctx);
|
|
110
|
+
} else if (c.type === 'WindowFunction') {
|
|
111
|
+
expr = this.compileOperand(c, ctx);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (c.alias) {
|
|
115
|
+
if (c.alias.includes('(')) return c.alias;
|
|
116
|
+
return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
|
|
117
|
+
}
|
|
118
|
+
return expr;
|
|
119
|
+
}).join(', ');
|
|
120
|
+
|
|
121
|
+
const distinct = ast.distinct ? 'DISTINCT ' : '';
|
|
122
|
+
const from = `${this.quoteIdentifier(ast.from.name)}`;
|
|
123
|
+
|
|
124
|
+
const joins = ast.joins.map(j => {
|
|
125
|
+
const table = this.quoteIdentifier(j.table.name);
|
|
126
|
+
const cond = this.compileExpression(j.condition, ctx);
|
|
127
|
+
return `${j.kind} JOIN ${table} ON ${cond}`;
|
|
128
|
+
}).join(' ');
|
|
129
|
+
const whereClause = this.compileWhere(ast.where, ctx);
|
|
130
|
+
|
|
131
|
+
const groupBy = ast.groupBy && ast.groupBy.length > 0
|
|
132
|
+
? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
|
|
133
|
+
: '';
|
|
134
|
+
|
|
135
|
+
const having = ast.having
|
|
136
|
+
? ` HAVING ${this.compileExpression(ast.having, ctx)}`
|
|
137
|
+
: '';
|
|
138
|
+
|
|
139
|
+
const orderBy = this.compileOrderBy(ast);
|
|
140
|
+
const pagination = this.compilePagination(ast, orderBy);
|
|
141
|
+
|
|
142
|
+
if (pagination) {
|
|
143
|
+
return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private compileOrderBy(ast: SelectQueryNode): string {
|
|
150
|
+
if (!ast.orderBy || ast.orderBy.length === 0) return '';
|
|
151
|
+
return ' ORDER BY ' + ast.orderBy
|
|
152
|
+
.map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`)
|
|
153
|
+
.join(', ');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private compilePagination(ast: SelectQueryNode, orderBy: string): string {
|
|
157
|
+
const hasLimit = ast.limit !== undefined;
|
|
158
|
+
const hasOffset = ast.offset !== undefined;
|
|
159
|
+
if (!hasLimit && !hasOffset) return '';
|
|
160
|
+
|
|
161
|
+
const off = ast.offset ?? 0;
|
|
162
|
+
const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
|
|
163
|
+
let pagination = `${orderClause} OFFSET ${off} ROWS`;
|
|
164
|
+
if (hasLimit) {
|
|
165
|
+
pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
|
|
166
|
+
}
|
|
167
|
+
return pagination;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private compileCtes(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
171
|
+
if (!ast.ctes || ast.ctes.length === 0) return '';
|
|
172
|
+
// MSSQL does not use RECURSIVE keyword, but supports recursion when CTE references itself.
|
|
173
|
+
const defs = ast.ctes.map(cte => {
|
|
174
|
+
const name = this.quoteIdentifier(cte.name);
|
|
175
|
+
const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
|
|
176
|
+
const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, '');
|
|
177
|
+
return `${name}${cols} AS (${query})`;
|
|
178
|
+
}).join(', ');
|
|
179
|
+
return `WITH ${defs} `;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private wrapSetOperand(sql: string): string {
|
|
183
|
+
const trimmed = sql.trim().replace(/;$/, '');
|
|
184
|
+
return `(${trimmed})`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -35,12 +35,11 @@ export class PostgresDialect extends SqlDialectBase {
|
|
|
35
35
|
|
|
36
36
|
protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
|
|
37
37
|
if (!returning || returning.length === 0) return '';
|
|
38
|
-
const columns = returning
|
|
39
|
-
.map(column => {
|
|
40
|
-
const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
|
|
41
|
-
return `${tablePart}${this.quoteIdentifier(column.name)}`;
|
|
42
|
-
})
|
|
43
|
-
.join(', ');
|
|
38
|
+
const columns = this.formatReturningColumns(returning);
|
|
44
39
|
return ` RETURNING ${columns}`;
|
|
45
40
|
}
|
|
41
|
+
|
|
42
|
+
supportsReturning(): boolean {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
46
45
|
}
|
|
@@ -35,12 +35,11 @@ export class SqliteDialect extends SqlDialectBase {
|
|
|
35
35
|
|
|
36
36
|
protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
|
|
37
37
|
if (!returning || returning.length === 0) return '';
|
|
38
|
-
const columns = returning
|
|
39
|
-
.map(column => {
|
|
40
|
-
const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
|
|
41
|
-
return `${tablePart}${this.quoteIdentifier(column.name)}`;
|
|
42
|
-
})
|
|
43
|
-
.join(', ');
|
|
38
|
+
const columns = this.formatReturningColumns(returning);
|
|
44
39
|
return ` RETURNING ${columns}`;
|
|
45
40
|
}
|
|
41
|
+
|
|
42
|
+
supportsReturning(): boolean {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
46
45
|
}
|
package/src/orm/execute.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { Entity } from '../schema/types.js';
|
|
3
|
-
import { hydrateRows } from './hydration.js';
|
|
4
|
-
import { OrmContext } from './orm-context.js';
|
|
5
|
-
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
6
|
-
import { createEntityFromRow } from './entity.js';
|
|
3
|
+
import { hydrateRows } from './hydration.js';
|
|
4
|
+
import { OrmContext } from './orm-context.js';
|
|
5
|
+
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
6
|
+
import { createEntityFromRow, createEntityProxy } from './entity.js';
|
|
7
7
|
|
|
8
8
|
type Row = Record<string, any>;
|
|
9
9
|
|
|
@@ -22,15 +22,24 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
|
|
|
22
22
|
return rows;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export async function executeHydrated<TTable extends TableDef>(
|
|
26
|
-
ctx: OrmContext,
|
|
27
|
-
qb: SelectQueryBuilder<any, TTable>
|
|
28
|
-
): Promise<Entity<TTable>[]> {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
|
|
25
|
+
export async function executeHydrated<TTable extends TableDef>(
|
|
26
|
+
ctx: OrmContext,
|
|
27
|
+
qb: SelectQueryBuilder<any, TTable>
|
|
28
|
+
): Promise<Entity<TTable>[]> {
|
|
29
|
+
const ast = qb.getAST();
|
|
30
|
+
const compiled = ctx.dialect.compileSelect(ast);
|
|
31
|
+
const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
32
|
+
const rows = flattenResults(executed);
|
|
33
|
+
|
|
34
|
+
// Set-operation queries cannot be reliably hydrated and should not collapse duplicates.
|
|
35
|
+
if (ast.setOps && ast.setOps.length > 0) {
|
|
36
|
+
return rows.map(row =>
|
|
37
|
+
createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hydrated = hydrateRows(rows, qb.getHydrationPlan());
|
|
42
|
+
return hydrated.map(row =>
|
|
43
|
+
createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
|
|
44
|
+
);
|
|
45
|
+
}
|