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