metal-orm 1.1.24 → 1.1.25

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.
Files changed (31) hide show
  1. package/dist/index.cjs +539 -412
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +225 -176
  4. package/dist/index.d.ts +225 -176
  5. package/dist/index.js +519 -412
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/core/dialect/base/returning-strategy.ts +40 -39
  9. package/src/core/dialect/base/sql-compiler-set.ts +33 -0
  10. package/src/core/dialect/base/sql-dialect.ts +79 -38
  11. package/src/core/dialect/base/upsert-strategy.ts +45 -0
  12. package/src/core/dialect/capabilities/procedure-compiler.ts +10 -8
  13. package/src/core/dialect/mssql/compiler-factory.ts +12 -0
  14. package/src/core/dialect/mssql/delete-compiler.ts +40 -0
  15. package/src/core/dialect/mssql/index.ts +24 -371
  16. package/src/core/dialect/mssql/insert-compiler.ts +112 -0
  17. package/src/core/dialect/mssql/output.ts +46 -0
  18. package/src/core/dialect/mssql/procedure-compiler.ts +81 -0
  19. package/src/core/dialect/mssql/select-compiler.ts +116 -0
  20. package/src/core/dialect/mssql/update-compiler.ts +37 -0
  21. package/src/core/dialect/mysql/index.ts +24 -117
  22. package/src/core/dialect/mysql/procedure-compiler.ts +67 -0
  23. package/src/core/dialect/mysql/upsert.ts +42 -0
  24. package/src/core/dialect/postgres/index.ts +34 -101
  25. package/src/core/dialect/postgres/procedure-compiler.ts +41 -0
  26. package/src/core/dialect/postgres/returning.ts +4 -0
  27. package/src/core/dialect/postgres/upsert.ts +43 -0
  28. package/src/core/dialect/sqlite/index.ts +15 -70
  29. package/src/core/dialect/sqlite/returning.ts +30 -0
  30. package/src/core/dialect/sqlite/upsert.ts +43 -0
  31. package/src/index.ts +22 -10
@@ -1,393 +1,46 @@
1
- import { CompilerContext } from '../abstract.js';
1
+ import type { ProcedureCallNode } from '../../ast/procedure.js';
2
+ import type { JsonPathNode } from '../../ast/expression.js';
2
3
  import type { CompiledProcedureCall, ProcedureCompiler } from '../capabilities/procedure-compiler.js';
3
- import {
4
- SelectQueryNode,
5
- InsertQueryNode,
6
- UpdateQueryNode,
7
- DeleteQueryNode
8
- } from '../../ast/query.js';
9
- import { JsonPathNode, ColumnNode } from '../../ast/expression.js';
10
- import { MssqlFunctionStrategy } from './functions.js';
11
- import { OrderByCompiler } from '../base/orderby-compiler.js';
12
- import { JoinCompiler } from '../base/join-compiler.js';
13
4
  import { SqlDialectBase } from '../base/sql-dialect.js';
14
- import { ProcedureCallNode } from '../../ast/procedure.js';
15
-
16
- const sanitizeVariableSuffix = (value: string): string =>
17
- value.replace(/[^a-zA-Z0-9_]/g, '_');
18
-
19
- const toProcedureParamReference = (value: string): string =>
20
- value.startsWith('@') ? value : `@${value}`;
5
+ import { MssqlFunctionStrategy } from './functions.js';
6
+ import { createMssqlCompilerSet } from './compiler-factory.js';
7
+ import { MssqlOutputStrategy } from './output.js';
8
+ import { MssqlProcedureCompiler } from './procedure-compiler.js';
21
9
 
22
- /**
23
- * Microsoft SQL Server dialect implementation
24
- */
10
+ /** Microsoft SQL Server dialect assembled from backend compiler components. */
25
11
  export class SqlServerDialect extends SqlDialectBase implements ProcedureCompiler {
26
12
  protected readonly dialect = 'mssql';
27
- /**
28
- * Creates a new SqlServerDialect instance
29
- */
13
+ private readonly procedureCompiler: MssqlProcedureCompiler;
14
+
30
15
  public constructor() {
31
- super(new MssqlFunctionStrategy());
16
+ super({
17
+ functionStrategy: new MssqlFunctionStrategy(),
18
+ returningStrategy: new MssqlOutputStrategy(),
19
+ compilerFactory: createMssqlCompilerSet,
20
+ supportsDmlReturning: true
21
+ });
22
+
23
+ this.procedureCompiler = new MssqlProcedureCompiler({
24
+ quoteIdentifier: id => this.quoteIdentifier(id),
25
+ createCompilerContext: () => this.createCompilerContext(),
26
+ compileOperand: (node, ctx) => this.compileOperand(node, ctx)
27
+ });
32
28
  }
33
29
 
34
- /**
35
- * Quotes an identifier using SQL Server bracket syntax
36
- * @param id - Identifier to quote
37
- * @returns Quoted identifier
38
- */
39
30
  quoteIdentifier(id: string): string {
40
31
  return `[${id}]`;
41
32
  }
42
33
 
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
34
  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}')`;
35
+ const column = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
36
+ return `JSON_VALUE(${column}, '${node.path}')`;
52
37
  }
53
38
 
54
- /**
55
- * Formats parameter placeholders using SQL Server named parameter syntax
56
- * @param index - Parameter index
57
- * @returns Named parameter placeholder
58
- */
59
39
  protected formatPlaceholder(index: number): string {
60
40
  return `@p${index}`;
61
41
  }
62
42
 
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}`;
81
- }
82
-
83
- const compound = ast.setOps!
84
- .map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
85
- .join(' ');
86
-
87
- const orderBy = this.compileOrderBy(ast, ctx);
88
- const pagination = this.compilePagination(ast, orderBy);
89
- const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
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);
138
-
139
- if (c.alias) {
140
- if (c.alias.includes('(')) return c.alias;
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;
202
- }
203
-
204
- supportsDmlReturningClause(): boolean {
205
- return true;
206
- }
207
-
208
- protected compileReturning(returning: ColumnNode[] | undefined, _ctx: CompilerContext): string {
209
- void _ctx;
210
- return this.compileOutputClause(returning, 'inserted');
211
- }
212
-
213
- private compileOutputClause(returning: ColumnNode[] | undefined, prefix: 'inserted' | 'deleted'): string {
214
- if (!returning || returning.length === 0) return '';
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}`;
223
- }
224
-
225
- protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
226
- if (!ast.columns.length) {
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}`;
239
- }
240
-
241
- private compileMergeInsert(ast: InsertQueryNode, ctx: CompilerContext): string {
242
- const clause = ast.onConflict!;
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} `;
326
- }
327
-
328
43
  compileProcedureCall(ast: ProcedureCallNode): CompiledProcedureCall {
329
- const ctx = this.createCompilerContext();
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
- };
44
+ return this.procedureCompiler.compileProcedureCall(ast);
391
45
  }
392
-
393
46
  }
@@ -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
+ }