metal-orm 1.1.24 → 1.1.26
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/dist/index.cjs +770 -710
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -318
- package/dist/index.d.ts +294 -318
- package/dist/index.js +745 -709
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/dialect/abstract.ts +7 -229
- package/src/core/dialect/base/returning-strategy.ts +40 -39
- package/src/core/dialect/base/sql-compiler-set.ts +33 -0
- package/src/core/dialect/base/sql-dialect-composer.ts +294 -0
- package/src/core/dialect/base/standard-sql-services.ts +2 -6
- package/src/core/dialect/base/upsert-strategy.ts +44 -0
- package/src/core/dialect/capabilities/procedure-compiler.ts +10 -8
- package/src/core/dialect/dialect-factory.ts +17 -49
- package/src/core/dialect/mssql/compiler-factory.ts +12 -0
- package/src/core/dialect/mssql/delete-compiler.ts +40 -0
- package/src/core/dialect/mssql/index.ts +52 -370
- package/src/core/dialect/mssql/insert-compiler.ts +112 -0
- package/src/core/dialect/mssql/output.ts +46 -0
- package/src/core/dialect/mssql/procedure-compiler.ts +81 -0
- package/src/core/dialect/mssql/select-compiler.ts +116 -0
- package/src/core/dialect/mssql/update-compiler.ts +37 -0
- package/src/core/dialect/mysql/index.ts +66 -126
- package/src/core/dialect/mysql/procedure-compiler.ts +67 -0
- package/src/core/dialect/mysql/upsert.ts +42 -0
- package/src/core/dialect/postgres/index.ts +75 -114
- package/src/core/dialect/postgres/procedure-compiler.ts +41 -0
- package/src/core/dialect/postgres/returning.ts +4 -0
- package/src/core/dialect/postgres/upsert.ts +43 -0
- package/src/core/dialect/sqlite/index.ts +65 -90
- package/src/core/dialect/sqlite/returning.ts +30 -0
- package/src/core/dialect/sqlite/upsert.ts +43 -0
- package/src/index.ts +22 -10
- package/src/core/dialect/base/sql-dialect.ts +0 -176
|
@@ -1,393 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import type { ProcedureCallNode } from '../../ast/procedure.js';
|
|
2
|
+
import type { JsonPathNode } from '../../ast/expression.js';
|
|
3
|
+
import type {
|
|
4
|
+
DeleteQueryNode,
|
|
5
5
|
InsertQueryNode,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
SelectQueryNode,
|
|
7
|
+
UpdateQueryNode
|
|
8
8
|
} from '../../ast/query.js';
|
|
9
|
-
import {
|
|
9
|
+
import type { CompiledQuery, Dialect } from '../abstract.js';
|
|
10
|
+
import type { CompiledProcedureCall, ProcedureCompiler } from '../capabilities/procedure-compiler.js';
|
|
11
|
+
import { composeSqlDialect } from '../base/sql-dialect-composer.js';
|
|
10
12
|
import { MssqlFunctionStrategy } from './functions.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Quotes an identifier using SQL Server bracket syntax
|
|
36
|
-
* @param id - Identifier to quote
|
|
37
|
-
* @returns Quoted identifier
|
|
38
|
-
*/
|
|
39
|
-
quoteIdentifier(id: string): string {
|
|
40
|
-
return `[${id}]`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Compiles JSON path expression using SQL Server syntax
|
|
45
|
-
* @param node - JSON path node
|
|
46
|
-
* @returns SQL Server JSON path expression
|
|
47
|
-
*/
|
|
48
|
-
protected compileJsonPath(node: JsonPathNode): string {
|
|
49
|
-
const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
|
|
50
|
-
// SQL Server uses JSON_VALUE(col, '$.path')
|
|
51
|
-
return `JSON_VALUE(${col}, '${node.path}')`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Formats parameter placeholders using SQL Server named parameter syntax
|
|
56
|
-
* @param index - Parameter index
|
|
57
|
-
* @returns Named parameter placeholder
|
|
58
|
-
*/
|
|
59
|
-
protected formatPlaceholder(index: number): string {
|
|
60
|
-
return `@p${index}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Compiles SELECT query AST to SQL Server SQL
|
|
65
|
-
* @param ast - Query AST
|
|
66
|
-
* @param ctx - Compiler context
|
|
67
|
-
* @returns SQL Server SQL string
|
|
68
|
-
*/
|
|
69
|
-
protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
70
|
-
const hasSetOps = !!(ast.setOps && ast.setOps.length);
|
|
71
|
-
const ctes = this.compileCtes(ast, ctx);
|
|
72
|
-
|
|
73
|
-
const baseAst: SelectQueryNode = hasSetOps
|
|
74
|
-
? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
|
|
75
|
-
: ast;
|
|
76
|
-
|
|
77
|
-
const baseSelect = this.compileSelectCoreForMssql(baseAst, ctx);
|
|
78
|
-
|
|
79
|
-
if (!hasSetOps) {
|
|
80
|
-
return `${ctes}${baseSelect}`;
|
|
13
|
+
import { createMssqlCompilerSet } from './compiler-factory.js';
|
|
14
|
+
import { MssqlOutputStrategy } from './output.js';
|
|
15
|
+
import { MssqlProcedureCompiler } from './procedure-compiler.js';
|
|
16
|
+
|
|
17
|
+
const quoteIdentifier = (id: string): string => `[${id}]`;
|
|
18
|
+
|
|
19
|
+
export type SqlServerDialectImplementation = Dialect & ProcedureCompiler;
|
|
20
|
+
|
|
21
|
+
/** Creates the SQL Server dialect entirely from composable compiler components. */
|
|
22
|
+
export const createSqlServerDialect = (): SqlServerDialectImplementation => {
|
|
23
|
+
const composition = composeSqlDialect({
|
|
24
|
+
name: 'mssql',
|
|
25
|
+
quoteIdentifier,
|
|
26
|
+
formatPlaceholder: index => `@p${index}`,
|
|
27
|
+
functionStrategy: new MssqlFunctionStrategy(),
|
|
28
|
+
returningStrategy: new MssqlOutputStrategy(),
|
|
29
|
+
compilerFactory: createMssqlCompilerSet,
|
|
30
|
+
supportsDmlReturning: true,
|
|
31
|
+
compileJsonPath(node: JsonPathNode): string {
|
|
32
|
+
const column = `${quoteIdentifier(node.column.table)}.${quoteIdentifier(node.column.name)}`;
|
|
33
|
+
return `JSON_VALUE(${column}, '${node.path}')`;
|
|
81
34
|
}
|
|
35
|
+
});
|
|
82
36
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
37
|
+
const procedures = new MssqlProcedureCompiler(composition.runtime);
|
|
38
|
+
return {
|
|
39
|
+
...composition.dialect,
|
|
40
|
+
compileProcedureCall: ast => procedures.compileProcedureCall(ast)
|
|
41
|
+
};
|
|
42
|
+
};
|
|
86
43
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const tail = pagination || orderBy;
|
|
91
|
-
return `${ctes}${combined}${tail}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
95
|
-
if (ast.using) {
|
|
96
|
-
throw new Error('DELETE ... USING is not supported in the MSSQL dialect; use join() instead.');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (ast.from.type !== 'Table') {
|
|
100
|
-
throw new Error('DELETE only supports base tables in the MSSQL dialect.');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const alias = ast.from.alias ?? ast.from.name;
|
|
104
|
-
const target = this.compileTableReference(ast.from);
|
|
105
|
-
const joins = JoinCompiler.compileJoins(
|
|
106
|
-
ast.joins,
|
|
107
|
-
ctx,
|
|
108
|
-
this.compileFrom.bind(this),
|
|
109
|
-
this.compileExpression.bind(this)
|
|
110
|
-
);
|
|
111
|
-
const whereClause = this.compileWhere(ast.where, ctx);
|
|
112
|
-
const returning = this.compileOutputClause(ast.returning, 'deleted');
|
|
113
|
-
return `DELETE ${this.quoteIdentifier(alias)}${returning} FROM ${target}${joins}${whereClause}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
|
|
117
|
-
const target = this.compileTableReference(ast.table);
|
|
118
|
-
const assignments = this.compileUpdateAssignments(ast.set, ast.table, ctx);
|
|
119
|
-
const output = this.compileReturning(ast.returning, ctx);
|
|
120
|
-
const fromClause = ast.from ? ` FROM ${this.compileFrom(ast.from, ctx)}` : '';
|
|
121
|
-
const joins = ast.joins
|
|
122
|
-
? ast.joins.map(j => {
|
|
123
|
-
const table = this.compileFrom(j.table, ctx);
|
|
124
|
-
const cond = this.compileExpression(j.condition, ctx);
|
|
125
|
-
return ` ${j.kind} JOIN ${table} ON ${cond}`;
|
|
126
|
-
}).join('')
|
|
127
|
-
: '';
|
|
128
|
-
const whereClause = this.compileWhere(ast.where, ctx);
|
|
129
|
-
return `UPDATE ${target} SET ${assignments}${output}${fromClause}${joins}${whereClause}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private compileSelectCoreForMssql(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
133
|
-
const columns = ast.columns.map(c => {
|
|
134
|
-
// Default to full operand compilation for all projection node types (Function, Column, Cast, Case, Window, etc)
|
|
135
|
-
const expr = c.type === 'Column'
|
|
136
|
-
? `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`
|
|
137
|
-
: this.compileOperand(c as unknown as import('../../ast/expression.js').OperandNode, ctx);
|
|
44
|
+
/** Ergonomic constructor facade over the composed SQL Server dialect. */
|
|
45
|
+
export class SqlServerDialect implements Dialect, ProcedureCompiler {
|
|
46
|
+
private readonly impl: SqlServerDialectImplementation = createSqlServerDialect();
|
|
138
47
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
|
|
142
|
-
}
|
|
143
|
-
return expr;
|
|
144
|
-
}).join(', ');
|
|
145
|
-
|
|
146
|
-
const distinct = ast.distinct ? 'DISTINCT ' : '';
|
|
147
|
-
const from = this.compileFrom(ast.from, ctx);
|
|
148
|
-
|
|
149
|
-
const joins = ast.joins.map(j => {
|
|
150
|
-
const table = this.compileFrom(j.table, ctx);
|
|
151
|
-
const cond = this.compileExpression(j.condition, ctx);
|
|
152
|
-
return `${j.kind} JOIN ${table} ON ${cond}`;
|
|
153
|
-
}).join(' ');
|
|
154
|
-
const whereClause = this.compileWhere(ast.where, ctx);
|
|
155
|
-
|
|
156
|
-
const groupBy = ast.groupBy && ast.groupBy.length > 0
|
|
157
|
-
? ' GROUP BY ' + ast.groupBy.map(term => this.compileOrderingTerm(term, ctx)).join(', ')
|
|
158
|
-
: '';
|
|
159
|
-
|
|
160
|
-
const having = ast.having
|
|
161
|
-
? ` HAVING ${this.compileExpression(ast.having, ctx)}`
|
|
162
|
-
: '';
|
|
163
|
-
|
|
164
|
-
const orderBy = this.compileOrderBy(ast, ctx);
|
|
165
|
-
const pagination = this.compilePagination(ast, orderBy);
|
|
166
|
-
|
|
167
|
-
if (pagination) {
|
|
168
|
-
return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private compileOrderBy(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
175
|
-
return OrderByCompiler.compileOrderBy(
|
|
176
|
-
ast,
|
|
177
|
-
term => this.compileOrderingTerm(term, ctx),
|
|
178
|
-
this.renderOrderByNulls.bind(this),
|
|
179
|
-
this.renderOrderByCollation.bind(this)
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private compilePagination(ast: SelectQueryNode, orderBy: string): string {
|
|
184
|
-
const hasLimit = ast.limit !== undefined;
|
|
185
|
-
const hasOffset = ast.offset !== undefined;
|
|
186
|
-
if (!hasLimit && !hasOffset) return '';
|
|
187
|
-
|
|
188
|
-
const off = ast.offset ?? 0;
|
|
189
|
-
let orderClause = orderBy;
|
|
190
|
-
if (!orderClause) {
|
|
191
|
-
// SQL Server requires ORDER BY items to appear in the SELECT list when DISTINCT is used.
|
|
192
|
-
// For paginated DISTINCT queries without explicit ORDER BY, use ORDER BY 1 (first projection).
|
|
193
|
-
orderClause = ast.distinct && ast.distinct.length > 0
|
|
194
|
-
? ' ORDER BY 1'
|
|
195
|
-
: ' ORDER BY (SELECT NULL)';
|
|
196
|
-
}
|
|
197
|
-
let pagination = `${orderClause} OFFSET ${off} ROWS`;
|
|
198
|
-
if (hasLimit) {
|
|
199
|
-
pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
|
|
200
|
-
}
|
|
201
|
-
return pagination;
|
|
48
|
+
quoteIdentifier(id: string): string {
|
|
49
|
+
return this.impl.quoteIdentifier(id);
|
|
202
50
|
}
|
|
203
51
|
|
|
204
52
|
supportsDmlReturningClause(): boolean {
|
|
205
|
-
return
|
|
53
|
+
return this.impl.supportsDmlReturningClause();
|
|
206
54
|
}
|
|
207
55
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return this.compileOutputClause(returning, 'inserted');
|
|
56
|
+
compileSelect(ast: SelectQueryNode): CompiledQuery {
|
|
57
|
+
return this.impl.compileSelect(ast);
|
|
211
58
|
}
|
|
212
59
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const columns = returning
|
|
216
|
-
.map(column => {
|
|
217
|
-
const colName = this.quoteIdentifier(column.name);
|
|
218
|
-
const alias = column.alias ? ` AS ${this.quoteIdentifier(column.alias)}` : '';
|
|
219
|
-
return `${prefix}.${colName}${alias}`;
|
|
220
|
-
})
|
|
221
|
-
.join(', ');
|
|
222
|
-
return ` OUTPUT ${columns}`;
|
|
60
|
+
compileInsert(ast: InsertQueryNode): CompiledQuery {
|
|
61
|
+
return this.impl.compileInsert(ast);
|
|
223
62
|
}
|
|
224
63
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new Error('INSERT queries must specify columns.');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (ast.onConflict) {
|
|
231
|
-
return this.compileMergeInsert(ast, ctx);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const table = this.compileTableName(ast.into);
|
|
235
|
-
const columnList = ast.columns.map(column => this.quoteIdentifier(column.name)).join(', ');
|
|
236
|
-
const output = this.compileReturning(ast.returning, ctx);
|
|
237
|
-
const source = this.compileInsertValues(ast, ctx);
|
|
238
|
-
return `INSERT INTO ${table} (${columnList})${output} ${source}`;
|
|
64
|
+
compileUpdate(ast: UpdateQueryNode): CompiledQuery {
|
|
65
|
+
return this.impl.compileUpdate(ast);
|
|
239
66
|
}
|
|
240
67
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (clause.target.constraint) {
|
|
244
|
-
throw new Error('MSSQL MERGE does not support conflict target by constraint name.');
|
|
245
|
-
}
|
|
246
|
-
this.ensureConflictColumns(clause, 'MSSQL MERGE requires conflict columns for the ON clause.');
|
|
247
|
-
|
|
248
|
-
const table = this.compileTableName(ast.into);
|
|
249
|
-
const targetRef = this.quoteIdentifier(ast.into.alias ?? ast.into.name);
|
|
250
|
-
const sourceAlias = this.quoteIdentifier('src');
|
|
251
|
-
const sourceColumns = ast.columns.map(column => this.quoteIdentifier(column.name)).join(', ');
|
|
252
|
-
const usingSource = this.compileMergeUsingSource(ast, ctx);
|
|
253
|
-
const onClause = clause.target.columns
|
|
254
|
-
.map(column => `${targetRef}.${this.quoteIdentifier(column.name)} = ${sourceAlias}.${this.quoteIdentifier(column.name)}`)
|
|
255
|
-
.join(' AND ');
|
|
256
|
-
|
|
257
|
-
const branches: string[] = [];
|
|
258
|
-
if (clause.action.type === 'DoUpdate') {
|
|
259
|
-
if (!clause.action.set.length) {
|
|
260
|
-
throw new Error('MSSQL MERGE WHEN MATCHED UPDATE requires at least one assignment.');
|
|
261
|
-
}
|
|
262
|
-
const assignments = clause.action.set
|
|
263
|
-
.map(assignment => {
|
|
264
|
-
const target = `${targetRef}.${this.quoteIdentifier(assignment.column.name)}`;
|
|
265
|
-
const value = this.compileOperand(assignment.value, ctx);
|
|
266
|
-
return `${target} = ${value}`;
|
|
267
|
-
})
|
|
268
|
-
.join(', ');
|
|
269
|
-
const guard = clause.action.where
|
|
270
|
-
? ` AND ${this.compileExpression(clause.action.where, ctx)}`
|
|
271
|
-
: '';
|
|
272
|
-
branches.push(`WHEN MATCHED${guard} THEN UPDATE SET ${assignments}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const insertColumns = ast.columns.map(column => this.quoteIdentifier(column.name)).join(', ');
|
|
276
|
-
const insertValues = ast.columns
|
|
277
|
-
.map(column => `${sourceAlias}.${this.quoteIdentifier(column.name)}`)
|
|
278
|
-
.join(', ');
|
|
279
|
-
branches.push(`WHEN NOT MATCHED THEN INSERT (${insertColumns}) VALUES (${insertValues})`);
|
|
280
|
-
|
|
281
|
-
const output = this.compileReturning(ast.returning, ctx);
|
|
282
|
-
return `MERGE INTO ${table} USING ${usingSource} AS ${sourceAlias} (${sourceColumns}) ON ${onClause} ${branches.join(' ')}${output}`;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private compileMergeUsingSource(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
286
|
-
if (ast.source.type === 'InsertValues') {
|
|
287
|
-
if (!ast.source.rows.length) {
|
|
288
|
-
throw new Error('INSERT ... VALUES requires at least one row.');
|
|
289
|
-
}
|
|
290
|
-
const rows = ast.source.rows
|
|
291
|
-
.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`)
|
|
292
|
-
.join(', ');
|
|
293
|
-
return `(VALUES ${rows})`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const normalized = this.normalizeSelectAst(ast.source.query);
|
|
297
|
-
const selectSql = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, '');
|
|
298
|
-
return `(${selectSql})`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private compileInsertValues(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
302
|
-
const source = ast.source;
|
|
303
|
-
if (source.type === 'InsertValues') {
|
|
304
|
-
if (!source.rows.length) {
|
|
305
|
-
throw new Error('INSERT ... VALUES requires at least one row.');
|
|
306
|
-
}
|
|
307
|
-
const values = source.rows
|
|
308
|
-
.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`)
|
|
309
|
-
.join(', ');
|
|
310
|
-
return `VALUES ${values}`;
|
|
311
|
-
}
|
|
312
|
-
const normalized = this.normalizeSelectAst(source.query);
|
|
313
|
-
return this.compileSelectAst(normalized, ctx).trim();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private compileCtes(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
317
|
-
if (!ast.ctes || ast.ctes.length === 0) return '';
|
|
318
|
-
// MSSQL does not use RECURSIVE keyword, but supports recursion when CTE references itself.
|
|
319
|
-
const defs = ast.ctes.map(cte => {
|
|
320
|
-
const name = this.quoteIdentifier(cte.name);
|
|
321
|
-
const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
|
|
322
|
-
const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, '');
|
|
323
|
-
return `${name}${cols} AS (${query})`;
|
|
324
|
-
}).join(', ');
|
|
325
|
-
return `WITH ${defs} `;
|
|
68
|
+
compileDelete(ast: DeleteQueryNode): CompiledQuery {
|
|
69
|
+
return this.impl.compileDelete(ast);
|
|
326
70
|
}
|
|
327
71
|
|
|
328
72
|
compileProcedureCall(ast: ProcedureCallNode): CompiledProcedureCall {
|
|
329
|
-
|
|
330
|
-
const qualifiedName = ast.ref.schema
|
|
331
|
-
? `${this.quoteIdentifier(ast.ref.schema)}.${this.quoteIdentifier(ast.ref.name)}`
|
|
332
|
-
: this.quoteIdentifier(ast.ref.name);
|
|
333
|
-
|
|
334
|
-
const declarations: string[] = [];
|
|
335
|
-
const assignments: string[] = [];
|
|
336
|
-
const execArgs: string[] = [];
|
|
337
|
-
const outVars: Array<{ variable: string; name: string }> = [];
|
|
338
|
-
|
|
339
|
-
ast.params.forEach((param, index) => {
|
|
340
|
-
const targetParam = toProcedureParamReference(param.name);
|
|
341
|
-
if (param.direction === 'in') {
|
|
342
|
-
if (!param.value) {
|
|
343
|
-
throw new Error(`Procedure parameter "${param.name}" requires a value for direction "in".`);
|
|
344
|
-
}
|
|
345
|
-
execArgs.push(`${targetParam} = ${this.compileOperand(param.value, ctx)}`);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!param.dbType) {
|
|
350
|
-
throw new Error(
|
|
351
|
-
`MSSQL procedure parameter "${param.name}" requires "dbType" for direction "${param.direction}".`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const suffix = sanitizeVariableSuffix(param.name || `p${index + 1}`);
|
|
356
|
-
const variable = `@__metal_${suffix}_${index + 1}`;
|
|
357
|
-
declarations.push(`DECLARE ${variable} ${param.dbType};`);
|
|
358
|
-
|
|
359
|
-
if (param.direction === 'inout') {
|
|
360
|
-
if (!param.value) {
|
|
361
|
-
throw new Error(`Procedure parameter "${param.name}" requires a value for direction "inout".`);
|
|
362
|
-
}
|
|
363
|
-
assignments.push(`SET ${variable} = ${this.compileOperand(param.value, ctx)};`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
execArgs.push(`${targetParam} = ${variable} OUTPUT`);
|
|
367
|
-
outVars.push({ variable, name: param.name });
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
const statements: string[] = [];
|
|
371
|
-
if (declarations.length) statements.push(...declarations);
|
|
372
|
-
if (assignments.length) statements.push(...assignments);
|
|
373
|
-
const argsSql = execArgs.length ? ` ${execArgs.join(', ')}` : '';
|
|
374
|
-
statements.push(`EXEC ${qualifiedName}${argsSql};`);
|
|
375
|
-
|
|
376
|
-
if (outVars.length) {
|
|
377
|
-
const selectOut = outVars
|
|
378
|
-
.map(({ variable, name }) => `${variable} AS ${this.quoteIdentifier(name)}`)
|
|
379
|
-
.join(', ');
|
|
380
|
-
statements.push(`SELECT ${selectOut};`);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
sql: statements.join(' '),
|
|
385
|
-
params: [...ctx.params],
|
|
386
|
-
outParams: {
|
|
387
|
-
source: outVars.length ? 'lastResultSet' : 'none',
|
|
388
|
-
names: outVars.map(item => item.name)
|
|
389
|
-
}
|
|
390
|
-
};
|
|
73
|
+
return this.impl.compileProcedureCall(ast);
|
|
391
74
|
}
|
|
392
|
-
|
|
393
75
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { InsertQueryNode } from '../../ast/query.js';
|
|
2
|
+
import type { CompilerContext } from '../abstract.js';
|
|
3
|
+
import type { SqlAstCompiler } from '../base/sql-compiler-set.js';
|
|
4
|
+
import type { StandardSqlCompilerServices } from '../base/standard-sql-services.js';
|
|
5
|
+
import type { StandardSqlSourceCompiler } from '../base/standard-sql-source-compiler.js';
|
|
6
|
+
|
|
7
|
+
export class MssqlInsertCompiler implements SqlAstCompiler<InsertQueryNode> {
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly services: StandardSqlCompilerServices,
|
|
10
|
+
private readonly sources: StandardSqlSourceCompiler
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
compile(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
14
|
+
if (!ast.columns.length) {
|
|
15
|
+
throw new Error('INSERT queries must specify columns.');
|
|
16
|
+
}
|
|
17
|
+
if (ast.onConflict) return this.compileMerge(ast, ctx);
|
|
18
|
+
|
|
19
|
+
const table = this.sources.compileTableName(ast.into);
|
|
20
|
+
const columns = ast.columns
|
|
21
|
+
.map(column => this.services.quoteIdentifier(column.name))
|
|
22
|
+
.join(', ');
|
|
23
|
+
const output = this.services.compileReturning(ast.returning, ctx);
|
|
24
|
+
const source = this.compileInsertSource(ast, ctx);
|
|
25
|
+
return `INSERT INTO ${table} (${columns})${output} ${source}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private compileMerge(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
29
|
+
const clause = ast.onConflict!;
|
|
30
|
+
if (clause.target.constraint) {
|
|
31
|
+
throw new Error('MSSQL MERGE does not support conflict target by constraint name.');
|
|
32
|
+
}
|
|
33
|
+
if (!clause.target.columns.length) {
|
|
34
|
+
throw new Error('MSSQL MERGE requires conflict columns for the ON clause.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const table = this.sources.compileTableName(ast.into);
|
|
38
|
+
const targetRef = this.services.quoteIdentifier(ast.into.alias ?? ast.into.name);
|
|
39
|
+
const sourceAlias = this.services.quoteIdentifier('src');
|
|
40
|
+
const sourceColumns = ast.columns
|
|
41
|
+
.map(column => this.services.quoteIdentifier(column.name))
|
|
42
|
+
.join(', ');
|
|
43
|
+
const usingSource = this.compileMergeUsingSource(ast, ctx);
|
|
44
|
+
const onClause = clause.target.columns
|
|
45
|
+
.map(column =>
|
|
46
|
+
`${targetRef}.${this.services.quoteIdentifier(column.name)} = ${sourceAlias}.${this.services.quoteIdentifier(column.name)}`
|
|
47
|
+
)
|
|
48
|
+
.join(' AND ');
|
|
49
|
+
|
|
50
|
+
const branches: string[] = [];
|
|
51
|
+
if (clause.action.type === 'DoUpdate') {
|
|
52
|
+
if (!clause.action.set.length) {
|
|
53
|
+
throw new Error('MSSQL MERGE WHEN MATCHED UPDATE requires at least one assignment.');
|
|
54
|
+
}
|
|
55
|
+
const assignments = clause.action.set
|
|
56
|
+
.map(assignment => {
|
|
57
|
+
const target = `${targetRef}.${this.services.quoteIdentifier(assignment.column.name)}`;
|
|
58
|
+
const value = this.services.compileOperand(assignment.value, ctx);
|
|
59
|
+
return `${target} = ${value}`;
|
|
60
|
+
})
|
|
61
|
+
.join(', ');
|
|
62
|
+
const guard = clause.action.where
|
|
63
|
+
? ` AND ${this.services.compileExpression(clause.action.where, ctx)}`
|
|
64
|
+
: '';
|
|
65
|
+
branches.push(`WHEN MATCHED${guard} THEN UPDATE SET ${assignments}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const insertColumns = ast.columns
|
|
69
|
+
.map(column => this.services.quoteIdentifier(column.name))
|
|
70
|
+
.join(', ');
|
|
71
|
+
const insertValues = ast.columns
|
|
72
|
+
.map(column => `${sourceAlias}.${this.services.quoteIdentifier(column.name)}`)
|
|
73
|
+
.join(', ');
|
|
74
|
+
branches.push(`WHEN NOT MATCHED THEN INSERT (${insertColumns}) VALUES (${insertValues})`);
|
|
75
|
+
|
|
76
|
+
const output = this.services.compileReturning(ast.returning, ctx);
|
|
77
|
+
return `MERGE INTO ${table} USING ${usingSource} AS ${sourceAlias} (${sourceColumns}) ON ${onClause} ${branches.join(' ')}${output}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private compileMergeUsingSource(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
81
|
+
if (ast.source.type === 'InsertValues') {
|
|
82
|
+
if (!ast.source.rows.length) {
|
|
83
|
+
throw new Error('INSERT ... VALUES requires at least one row.');
|
|
84
|
+
}
|
|
85
|
+
const rows = ast.source.rows
|
|
86
|
+
.map(row => `(${row.map(value => this.services.compileOperand(value, ctx)).join(', ')})`)
|
|
87
|
+
.join(', ');
|
|
88
|
+
return `(VALUES ${rows})`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const normalized = this.services.normalizeSelectAst(ast.source.query);
|
|
92
|
+
const selectSql = this.sources.stripTrailingSemicolon(
|
|
93
|
+
this.services.compileSelectAst(normalized, ctx)
|
|
94
|
+
);
|
|
95
|
+
return `(${selectSql})`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private compileInsertSource(ast: InsertQueryNode, ctx: CompilerContext): string {
|
|
99
|
+
if (ast.source.type === 'InsertValues') {
|
|
100
|
+
if (!ast.source.rows.length) {
|
|
101
|
+
throw new Error('INSERT ... VALUES requires at least one row.');
|
|
102
|
+
}
|
|
103
|
+
const values = ast.source.rows
|
|
104
|
+
.map(row => `(${row.map(value => this.services.compileOperand(value, ctx)).join(', ')})`)
|
|
105
|
+
.join(', ');
|
|
106
|
+
return `VALUES ${values}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalized = this.services.normalizeSelectAst(ast.source.query);
|
|
110
|
+
return this.services.compileSelectAst(normalized, ctx).trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ColumnNode } from '../../ast/expression.js';
|
|
2
|
+
import type { CompilerContext } from '../abstract.js';
|
|
3
|
+
import type {
|
|
4
|
+
QuoteIdentifier,
|
|
5
|
+
ReturningStrategy
|
|
6
|
+
} from '../base/returning-strategy.js';
|
|
7
|
+
|
|
8
|
+
export type MssqlOutputPrefix = 'inserted' | 'deleted';
|
|
9
|
+
|
|
10
|
+
export class MssqlOutputStrategy implements ReturningStrategy {
|
|
11
|
+
compileReturning(
|
|
12
|
+
returning: ColumnNode[] | undefined,
|
|
13
|
+
_ctx: CompilerContext,
|
|
14
|
+
quoteIdentifier: QuoteIdentifier
|
|
15
|
+
): string {
|
|
16
|
+
void _ctx;
|
|
17
|
+
return this.compileOutput(returning, 'inserted', quoteIdentifier);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
compileOutput(
|
|
21
|
+
returning: ColumnNode[] | undefined,
|
|
22
|
+
prefix: MssqlOutputPrefix,
|
|
23
|
+
quoteIdentifier: QuoteIdentifier
|
|
24
|
+
): string {
|
|
25
|
+
if (!returning || returning.length === 0) return '';
|
|
26
|
+
const columns = returning
|
|
27
|
+
.map(column => {
|
|
28
|
+
const alias = column.alias ? ` AS ${quoteIdentifier(column.alias)}` : '';
|
|
29
|
+
return `${prefix}.${quoteIdentifier(column.name)}${alias}`;
|
|
30
|
+
})
|
|
31
|
+
.join(', ');
|
|
32
|
+
return ` OUTPUT ${columns}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
formatReturningColumns(
|
|
36
|
+
returning: ColumnNode[],
|
|
37
|
+
quoteIdentifier: QuoteIdentifier
|
|
38
|
+
): string {
|
|
39
|
+
return returning
|
|
40
|
+
.map(column => {
|
|
41
|
+
const alias = column.alias ? ` AS ${quoteIdentifier(column.alias)}` : '';
|
|
42
|
+
return `${quoteIdentifier(column.name)}${alias}`;
|
|
43
|
+
})
|
|
44
|
+
.join(', ');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ProcedureCallNode } from '../../ast/procedure.js';
|
|
2
|
+
import type {
|
|
3
|
+
CompiledProcedureCall,
|
|
4
|
+
ProcedureCompiler,
|
|
5
|
+
ProcedureCompilerServices
|
|
6
|
+
} from '../capabilities/procedure-compiler.js';
|
|
7
|
+
|
|
8
|
+
const sanitizeVariableSuffix = (value: string): string =>
|
|
9
|
+
value.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
10
|
+
|
|
11
|
+
const toProcedureParamReference = (value: string): string =>
|
|
12
|
+
value.startsWith('@') ? value : `@${value}`;
|
|
13
|
+
|
|
14
|
+
export class MssqlProcedureCompiler implements ProcedureCompiler {
|
|
15
|
+
constructor(private readonly services: ProcedureCompilerServices) {}
|
|
16
|
+
|
|
17
|
+
compileProcedureCall(ast: ProcedureCallNode): CompiledProcedureCall {
|
|
18
|
+
const ctx = this.services.createCompilerContext();
|
|
19
|
+
const qualifiedName = ast.ref.schema
|
|
20
|
+
? `${this.services.quoteIdentifier(ast.ref.schema)}.${this.services.quoteIdentifier(ast.ref.name)}`
|
|
21
|
+
: this.services.quoteIdentifier(ast.ref.name);
|
|
22
|
+
|
|
23
|
+
const declarations: string[] = [];
|
|
24
|
+
const assignments: string[] = [];
|
|
25
|
+
const execArgs: string[] = [];
|
|
26
|
+
const outVars: Array<{ variable: string; name: string }> = [];
|
|
27
|
+
|
|
28
|
+
ast.params.forEach((param, index) => {
|
|
29
|
+
const targetParam = toProcedureParamReference(param.name);
|
|
30
|
+
if (param.direction === 'in') {
|
|
31
|
+
if (!param.value) {
|
|
32
|
+
throw new Error(`Procedure parameter "${param.name}" requires a value for direction "in".`);
|
|
33
|
+
}
|
|
34
|
+
execArgs.push(`${targetParam} = ${this.services.compileOperand(param.value, ctx)}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!param.dbType) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`MSSQL procedure parameter "${param.name}" requires "dbType" for direction "${param.direction}".`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const suffix = sanitizeVariableSuffix(param.name || `p${index + 1}`);
|
|
45
|
+
const variable = `@__metal_${suffix}_${index + 1}`;
|
|
46
|
+
declarations.push(`DECLARE ${variable} ${param.dbType};`);
|
|
47
|
+
|
|
48
|
+
if (param.direction === 'inout') {
|
|
49
|
+
if (!param.value) {
|
|
50
|
+
throw new Error(`Procedure parameter "${param.name}" requires a value for direction "inout".`);
|
|
51
|
+
}
|
|
52
|
+
assignments.push(`SET ${variable} = ${this.services.compileOperand(param.value, ctx)};`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
execArgs.push(`${targetParam} = ${variable} OUTPUT`);
|
|
56
|
+
outVars.push({ variable, name: param.name });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const statements: string[] = [];
|
|
60
|
+
if (declarations.length) statements.push(...declarations);
|
|
61
|
+
if (assignments.length) statements.push(...assignments);
|
|
62
|
+
const argsSql = execArgs.length ? ` ${execArgs.join(', ')}` : '';
|
|
63
|
+
statements.push(`EXEC ${qualifiedName}${argsSql};`);
|
|
64
|
+
|
|
65
|
+
if (outVars.length) {
|
|
66
|
+
const selectOut = outVars
|
|
67
|
+
.map(({ variable, name }) => `${variable} AS ${this.services.quoteIdentifier(name)}`)
|
|
68
|
+
.join(', ');
|
|
69
|
+
statements.push(`SELECT ${selectOut};`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
sql: statements.join(' '),
|
|
74
|
+
params: [...ctx.params],
|
|
75
|
+
outParams: {
|
|
76
|
+
source: outVars.length ? 'lastResultSet' : 'none',
|
|
77
|
+
names: outVars.map(item => item.name)
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|