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,565 +1,578 @@
1
1
  import {
2
- SelectQueryNode,
3
- InsertQueryNode,
4
- UpdateQueryNode,
5
- DeleteQueryNode,
6
- SetOperationKind,
7
- CommonTableExpressionNode,
8
- OrderingTerm
9
- } from '../ast/query.js';
10
- import {
11
- ExpressionNode,
12
- BinaryExpressionNode,
13
- LogicalExpressionNode,
14
- NullExpressionNode,
15
- InExpressionNode,
16
- ExistsExpressionNode,
17
- LiteralNode,
18
- ColumnNode,
19
- OperandNode,
20
- FunctionNode,
21
- JsonPathNode,
22
- ScalarSubqueryNode,
23
- CaseExpressionNode,
24
- CastExpressionNode,
25
- WindowFunctionNode,
26
- BetweenExpressionNode,
27
- ArithmeticExpressionNode,
28
- BitwiseExpressionNode,
29
- CollateExpressionNode,
30
- AliasRefNode,
31
- isOperandNode
2
+ SelectQueryNode,
3
+ InsertQueryNode,
4
+ UpdateQueryNode,
5
+ DeleteQueryNode,
6
+ SetOperationKind,
7
+ CommonTableExpressionNode,
8
+ OrderingTerm
9
+ } from '../ast/query.js';
10
+ import {
11
+ ExpressionNode,
12
+ BinaryExpressionNode,
13
+ LogicalExpressionNode,
14
+ NullExpressionNode,
15
+ InExpressionNode,
16
+ ExistsExpressionNode,
17
+ LiteralNode,
18
+ ColumnNode,
19
+ OperandNode,
20
+ FunctionNode,
21
+ JsonPathNode,
22
+ ScalarSubqueryNode,
23
+ CaseExpressionNode,
24
+ CastExpressionNode,
25
+ WindowFunctionNode,
26
+ BetweenExpressionNode,
27
+ ArithmeticExpressionNode,
28
+ BitwiseExpressionNode,
29
+ CollateExpressionNode,
30
+ AliasRefNode,
31
+ isOperandNode
32
32
  } from '../ast/expression.js';
33
+ import { ProcedureCallNode } from '../ast/procedure.js';
33
34
  import { DialectName } from '../sql/sql.js';
34
- import type { FunctionStrategy } from '../functions/types.js';
35
- import { StandardFunctionStrategy } from '../functions/standard-strategy.js';
36
- import type { TableFunctionStrategy } from '../functions/table-types.js';
37
- import { StandardTableFunctionStrategy } from '../functions/standard-table-strategy.js';
38
-
39
- /**
40
- * Context for SQL compilation with parameter management
41
- */
42
- export interface CompilerContext {
43
- /** Array of parameters */
44
- params: unknown[];
45
- /** Function to add a parameter and get its placeholder */
46
- addParameter(value: unknown): string;
47
- }
48
-
49
- /**
50
- * Result of SQL compilation
51
- */
35
+ import type { FunctionStrategy } from '../functions/types.js';
36
+ import { StandardFunctionStrategy } from '../functions/standard-strategy.js';
37
+ import type { TableFunctionStrategy } from '../functions/table-types.js';
38
+ import { StandardTableFunctionStrategy } from '../functions/standard-table-strategy.js';
39
+
40
+ /**
41
+ * Context for SQL compilation with parameter management
42
+ */
43
+ export interface CompilerContext {
44
+ /** Array of parameters */
45
+ params: unknown[];
46
+ /** Function to add a parameter and get its placeholder */
47
+ addParameter(value: unknown): string;
48
+ }
49
+
50
+ /**
51
+ * Result of SQL compilation
52
+ */
52
53
  export interface CompiledQuery {
53
- /** Generated SQL string */
54
- sql: string;
55
- /** Parameters for the query */
54
+ /** Generated SQL string */
55
+ sql: string;
56
+ /** Parameters for the query */
56
57
  params: unknown[];
57
58
  }
58
59
 
59
- export interface SelectCompiler {
60
- compileSelect(ast: SelectQueryNode): CompiledQuery;
61
- }
62
-
63
- export interface InsertCompiler {
64
- compileInsert(ast: InsertQueryNode): CompiledQuery;
60
+ export interface CompiledProcedureCall extends CompiledQuery {
61
+ outParams: {
62
+ source: 'none' | 'firstResultSet' | 'lastResultSet';
63
+ names: string[];
64
+ };
65
65
  }
66
-
67
- export interface UpdateCompiler {
68
- compileUpdate(ast: UpdateQueryNode): CompiledQuery;
69
- }
70
-
71
- export interface DeleteCompiler {
72
- compileDelete(ast: DeleteQueryNode): CompiledQuery;
73
- }
74
-
75
- /**
76
- * Abstract base class for SQL dialect implementations
77
- */
66
+
67
+ export interface SelectCompiler {
68
+ compileSelect(ast: SelectQueryNode): CompiledQuery;
69
+ }
70
+
71
+ export interface InsertCompiler {
72
+ compileInsert(ast: InsertQueryNode): CompiledQuery;
73
+ }
74
+
75
+ export interface UpdateCompiler {
76
+ compileUpdate(ast: UpdateQueryNode): CompiledQuery;
77
+ }
78
+
79
+ export interface DeleteCompiler {
80
+ compileDelete(ast: DeleteQueryNode): CompiledQuery;
81
+ }
82
+
83
+ /**
84
+ * Abstract base class for SQL dialect implementations
85
+ */
78
86
  export abstract class Dialect
79
87
  implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler {
80
- /** Dialect identifier used for function rendering and formatting */
81
- protected abstract readonly dialect: DialectName;
82
-
83
- /**
84
- * Compiles a SELECT query AST to SQL
85
- * @param ast - Query AST to compile
86
- * @returns Compiled query with SQL and parameters
87
- */
88
- compileSelect(ast: SelectQueryNode): CompiledQuery {
89
- const ctx = this.createCompilerContext();
90
- const normalized = this.normalizeSelectAst(ast);
91
- const rawSql = this.compileSelectAst(normalized, ctx).trim();
92
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
93
- return {
94
- sql,
95
- params: [...ctx.params]
96
- };
97
- }
98
-
99
- compileInsert(ast: InsertQueryNode): CompiledQuery {
100
- const ctx = this.createCompilerContext();
101
- const rawSql = this.compileInsertAst(ast, ctx).trim();
102
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
103
- return {
104
- sql,
105
- params: [...ctx.params]
106
- };
107
- }
108
-
109
- compileUpdate(ast: UpdateQueryNode): CompiledQuery {
110
- const ctx = this.createCompilerContext();
111
- const rawSql = this.compileUpdateAst(ast, ctx).trim();
112
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
113
- return {
114
- sql,
115
- params: [...ctx.params]
116
- };
117
- }
118
-
88
+ /** Dialect identifier used for function rendering and formatting */
89
+ protected abstract readonly dialect: DialectName;
90
+
91
+ /**
92
+ * Compiles a SELECT query AST to SQL
93
+ * @param ast - Query AST to compile
94
+ * @returns Compiled query with SQL and parameters
95
+ */
96
+ compileSelect(ast: SelectQueryNode): CompiledQuery {
97
+ const ctx = this.createCompilerContext();
98
+ const normalized = this.normalizeSelectAst(ast);
99
+ const rawSql = this.compileSelectAst(normalized, ctx).trim();
100
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
101
+ return {
102
+ sql,
103
+ params: [...ctx.params]
104
+ };
105
+ }
106
+
107
+ compileInsert(ast: InsertQueryNode): CompiledQuery {
108
+ const ctx = this.createCompilerContext();
109
+ const rawSql = this.compileInsertAst(ast, ctx).trim();
110
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
111
+ return {
112
+ sql,
113
+ params: [...ctx.params]
114
+ };
115
+ }
116
+
117
+ compileUpdate(ast: UpdateQueryNode): CompiledQuery {
118
+ const ctx = this.createCompilerContext();
119
+ const rawSql = this.compileUpdateAst(ast, ctx).trim();
120
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
121
+ return {
122
+ sql,
123
+ params: [...ctx.params]
124
+ };
125
+ }
126
+
119
127
  compileDelete(ast: DeleteQueryNode): CompiledQuery {
120
- const ctx = this.createCompilerContext();
121
- const rawSql = this.compileDeleteAst(ast, ctx).trim();
122
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
123
- return {
124
- sql,
125
- params: [...ctx.params]
126
- };
127
- }
128
-
129
- supportsDmlReturningClause(): boolean {
130
- return false;
131
- }
132
-
133
- /**
134
- * Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
135
- * @param ast - Query AST
136
- * @param ctx - Compiler context
137
- * @returns SQL string
138
- */
139
- protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
140
-
141
- protected abstract compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string;
142
- protected abstract compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string;
143
- protected abstract compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string;
144
-
145
- /**
146
- * Quotes an SQL identifier (to be implemented by concrete dialects)
147
- * @param id - Identifier to quote
148
- * @returns Quoted identifier
149
- */
150
- abstract quoteIdentifier(id: string): string;
151
-
152
- /**
153
- * Compiles a WHERE clause
154
- * @param where - WHERE expression
155
- * @param ctx - Compiler context
156
- * @returns SQL WHERE clause or empty string
157
- */
158
- protected compileWhere(where: ExpressionNode | undefined, ctx: CompilerContext): string {
159
- if (!where) return '';
160
- return ` WHERE ${this.compileExpression(where, ctx)}`;
161
- }
162
-
163
- protected compileReturning(
164
- returning: ColumnNode[] | undefined,
165
- _ctx: CompilerContext
166
- ): string {
167
- void _ctx;
168
- if (!returning || returning.length === 0) return '';
169
- throw new Error('RETURNING is not supported by this dialect.');
170
- }
171
-
172
- /**
173
- * Generates subquery for EXISTS expressions
174
- * Rule: Always forces SELECT 1, ignoring column list
175
- * Maintains FROM, JOINs, WHERE, GROUP BY, ORDER BY, LIMIT/OFFSET
176
- * Does not add ';' at the end
177
- * @param ast - Query AST
178
- * @param ctx - Compiler context
179
- * @returns SQL for EXISTS subquery
180
- */
181
- protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
182
- const normalized = this.normalizeSelectAst(ast);
183
- const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, '');
184
-
185
- // When the subquery is a set operation, wrap it as a derived table to keep valid syntax.
186
- if (normalized.setOps && normalized.setOps.length > 0) {
187
- return `SELECT 1 FROM (${full}) AS _exists`;
188
- }
189
-
190
- const upper = full.toUpperCase();
191
- const fromIndex = upper.indexOf(' FROM ');
192
- if (fromIndex === -1) {
193
- return full;
194
- }
195
-
196
- const tail = full.slice(fromIndex);
197
- return `SELECT 1${tail}`;
198
- }
199
-
200
- /**
201
- * Creates a new compiler context
202
- * @returns Compiler context with parameter management
203
- */
204
- protected createCompilerContext(): CompilerContext {
205
- const params: unknown[] = [];
206
- let counter = 0;
207
- return {
208
- params,
209
- addParameter: (value: unknown) => {
210
- counter += 1;
211
- params.push(value);
212
- return this.formatPlaceholder(counter);
213
- }
214
- };
215
- }
216
-
217
- /**
218
- * Formats a parameter placeholder
219
- * @param index - Parameter index
220
- * @returns Formatted placeholder string
221
- */
222
- protected formatPlaceholder(_index: number): string {
223
- void _index;
224
- return '?';
225
- }
226
-
227
- /**
228
- * Whether the current dialect supports a given set operation.
229
- * Override in concrete dialects to restrict support.
230
- */
231
- protected supportsSetOperation(_kind: SetOperationKind): boolean {
232
- void _kind;
233
- return true;
234
- }
235
-
236
- /**
237
- * Validates set-operation semantics:
238
- * - Ensures the dialect supports requested operators.
239
- * - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
240
- * @param ast - Query to validate
241
- * @param isOutermost - Whether this node is the outermost compound query
242
- */
243
- protected validateSetOperations(ast: SelectQueryNode, isOutermost = true): void {
244
- const hasSetOps = !!(ast.setOps && ast.setOps.length);
245
- if (!isOutermost && (ast.orderBy || ast.limit !== undefined || ast.offset !== undefined)) {
246
- throw new Error('ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.');
247
- }
248
-
249
- if (hasSetOps) {
250
- for (const op of ast.setOps!) {
251
- if (!this.supportsSetOperation(op.operator)) {
252
- throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
253
- }
254
- this.validateSetOperations(op.query, false);
255
- }
256
- }
257
- }
258
-
259
- /**
260
- * Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
261
- * @param ast - Query AST
262
- * @returns Normalized AST without inner CTEs and a list of hoisted CTEs
263
- */
264
- private hoistCtes(ast: SelectQueryNode): { normalized: SelectQueryNode; hoistedCtes: CommonTableExpressionNode[] } {
265
- let hoisted: CommonTableExpressionNode[] = [];
266
-
267
- const normalizedSetOps = ast.setOps?.map(op => {
268
- const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
269
- const childCtes = child.ctes ?? [];
270
- if (childCtes.length) {
271
- hoisted = hoisted.concat(childCtes);
272
- }
273
- hoisted = hoisted.concat(childHoisted);
274
- const queryWithoutCtes = childCtes.length ? { ...child, ctes: undefined } : child;
275
- return { ...op, query: queryWithoutCtes };
276
- });
277
-
278
- const normalized: SelectQueryNode = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
279
- return { normalized, hoistedCtes: hoisted };
280
- }
281
-
282
- /**
283
- * Normalizes a SELECT AST before compilation (validation + CTE hoisting).
284
- * @param ast - Query AST
285
- * @returns Normalized query AST
286
- */
287
- protected normalizeSelectAst(ast: SelectQueryNode): SelectQueryNode {
288
- this.validateSetOperations(ast, true);
289
- const { normalized, hoistedCtes } = this.hoistCtes(ast);
290
- const combinedCtes = [...(normalized.ctes ?? []), ...hoistedCtes];
291
- return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
292
- }
293
-
294
- private readonly expressionCompilers: Map<string, (node: ExpressionNode, ctx: CompilerContext) => string>;
295
- private readonly operandCompilers: Map<string, (node: OperandNode, ctx: CompilerContext) => string>;
296
- protected readonly functionStrategy: FunctionStrategy;
297
- protected readonly tableFunctionStrategy: TableFunctionStrategy;
298
-
299
- protected constructor(functionStrategy?: FunctionStrategy, tableFunctionStrategy?: TableFunctionStrategy) {
300
- this.expressionCompilers = new Map();
301
- this.operandCompilers = new Map();
302
- this.functionStrategy = functionStrategy || new StandardFunctionStrategy();
303
- this.tableFunctionStrategy = tableFunctionStrategy || new StandardTableFunctionStrategy();
304
- this.registerDefaultOperandCompilers();
305
- this.registerDefaultExpressionCompilers();
128
+ const ctx = this.createCompilerContext();
129
+ const rawSql = this.compileDeleteAst(ast, ctx).trim();
130
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
131
+ return {
132
+ sql,
133
+ params: [...ctx.params]
134
+ };
306
135
  }
307
136
 
308
- /**
309
- * Creates a new Dialect instance (for testing purposes)
310
- * @param functionStrategy - Optional function strategy
311
- * @returns New Dialect instance
312
- */
313
- static create(functionStrategy?: FunctionStrategy, tableFunctionStrategy?: TableFunctionStrategy): Dialect {
314
- // Create a minimal concrete implementation for testing
315
- class TestDialect extends Dialect {
316
- protected readonly dialect: DialectName = 'sqlite';
317
- quoteIdentifier(id: string): string {
318
- return `"${id}"`;
319
- }
320
- protected compileSelectAst(): never {
321
- throw new Error('Not implemented');
322
- }
323
- protected compileInsertAst(): never {
324
- throw new Error('Not implemented');
325
- }
326
- protected compileUpdateAst(): never {
327
- throw new Error('Not implemented');
328
- }
137
+ abstract compileProcedureCall(ast: ProcedureCallNode): CompiledProcedureCall;
138
+
139
+ supportsDmlReturningClause(): boolean {
140
+ return false;
141
+ }
142
+
143
+ /**
144
+ * Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
145
+ * @param ast - Query AST
146
+ * @param ctx - Compiler context
147
+ * @returns SQL string
148
+ */
149
+ protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
150
+
151
+ protected abstract compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string;
152
+ protected abstract compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string;
153
+ protected abstract compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string;
154
+
155
+ /**
156
+ * Quotes an SQL identifier (to be implemented by concrete dialects)
157
+ * @param id - Identifier to quote
158
+ * @returns Quoted identifier
159
+ */
160
+ abstract quoteIdentifier(id: string): string;
161
+
162
+ /**
163
+ * Compiles a WHERE clause
164
+ * @param where - WHERE expression
165
+ * @param ctx - Compiler context
166
+ * @returns SQL WHERE clause or empty string
167
+ */
168
+ protected compileWhere(where: ExpressionNode | undefined, ctx: CompilerContext): string {
169
+ if (!where) return '';
170
+ return ` WHERE ${this.compileExpression(where, ctx)}`;
171
+ }
172
+
173
+ protected compileReturning(
174
+ returning: ColumnNode[] | undefined,
175
+ _ctx: CompilerContext
176
+ ): string {
177
+ void _ctx;
178
+ if (!returning || returning.length === 0) return '';
179
+ throw new Error('RETURNING is not supported by this dialect.');
180
+ }
181
+
182
+ /**
183
+ * Generates subquery for EXISTS expressions
184
+ * Rule: Always forces SELECT 1, ignoring column list
185
+ * Maintains FROM, JOINs, WHERE, GROUP BY, ORDER BY, LIMIT/OFFSET
186
+ * Does not add ';' at the end
187
+ * @param ast - Query AST
188
+ * @param ctx - Compiler context
189
+ * @returns SQL for EXISTS subquery
190
+ */
191
+ protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
192
+ const normalized = this.normalizeSelectAst(ast);
193
+ const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, '');
194
+
195
+ // When the subquery is a set operation, wrap it as a derived table to keep valid syntax.
196
+ if (normalized.setOps && normalized.setOps.length > 0) {
197
+ return `SELECT 1 FROM (${full}) AS _exists`;
198
+ }
199
+
200
+ const upper = full.toUpperCase();
201
+ const fromIndex = upper.indexOf(' FROM ');
202
+ if (fromIndex === -1) {
203
+ return full;
204
+ }
205
+
206
+ const tail = full.slice(fromIndex);
207
+ return `SELECT 1${tail}`;
208
+ }
209
+
210
+ /**
211
+ * Creates a new compiler context
212
+ * @returns Compiler context with parameter management
213
+ */
214
+ protected createCompilerContext(): CompilerContext {
215
+ const params: unknown[] = [];
216
+ let counter = 0;
217
+ return {
218
+ params,
219
+ addParameter: (value: unknown) => {
220
+ counter += 1;
221
+ params.push(value);
222
+ return this.formatPlaceholder(counter);
223
+ }
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Formats a parameter placeholder
229
+ * @param index - Parameter index
230
+ * @returns Formatted placeholder string
231
+ */
232
+ protected formatPlaceholder(_index: number): string {
233
+ void _index;
234
+ return '?';
235
+ }
236
+
237
+ /**
238
+ * Whether the current dialect supports a given set operation.
239
+ * Override in concrete dialects to restrict support.
240
+ */
241
+ protected supportsSetOperation(_kind: SetOperationKind): boolean {
242
+ void _kind;
243
+ return true;
244
+ }
245
+
246
+ /**
247
+ * Validates set-operation semantics:
248
+ * - Ensures the dialect supports requested operators.
249
+ * - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
250
+ * @param ast - Query to validate
251
+ * @param isOutermost - Whether this node is the outermost compound query
252
+ */
253
+ protected validateSetOperations(ast: SelectQueryNode, isOutermost = true): void {
254
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
255
+ if (!isOutermost && (ast.orderBy || ast.limit !== undefined || ast.offset !== undefined)) {
256
+ throw new Error('ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.');
257
+ }
258
+
259
+ if (hasSetOps) {
260
+ for (const op of ast.setOps!) {
261
+ if (!this.supportsSetOperation(op.operator)) {
262
+ throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
263
+ }
264
+ this.validateSetOperations(op.query, false);
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
271
+ * @param ast - Query AST
272
+ * @returns Normalized AST without inner CTEs and a list of hoisted CTEs
273
+ */
274
+ private hoistCtes(ast: SelectQueryNode): { normalized: SelectQueryNode; hoistedCtes: CommonTableExpressionNode[] } {
275
+ let hoisted: CommonTableExpressionNode[] = [];
276
+
277
+ const normalizedSetOps = ast.setOps?.map(op => {
278
+ const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
279
+ const childCtes = child.ctes ?? [];
280
+ if (childCtes.length) {
281
+ hoisted = hoisted.concat(childCtes);
282
+ }
283
+ hoisted = hoisted.concat(childHoisted);
284
+ const queryWithoutCtes = childCtes.length ? { ...child, ctes: undefined } : child;
285
+ return { ...op, query: queryWithoutCtes };
286
+ });
287
+
288
+ const normalized: SelectQueryNode = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
289
+ return { normalized, hoistedCtes: hoisted };
290
+ }
291
+
292
+ /**
293
+ * Normalizes a SELECT AST before compilation (validation + CTE hoisting).
294
+ * @param ast - Query AST
295
+ * @returns Normalized query AST
296
+ */
297
+ protected normalizeSelectAst(ast: SelectQueryNode): SelectQueryNode {
298
+ this.validateSetOperations(ast, true);
299
+ const { normalized, hoistedCtes } = this.hoistCtes(ast);
300
+ const combinedCtes = [...(normalized.ctes ?? []), ...hoistedCtes];
301
+ return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
302
+ }
303
+
304
+ private readonly expressionCompilers: Map<string, (node: ExpressionNode, ctx: CompilerContext) => string>;
305
+ private readonly operandCompilers: Map<string, (node: OperandNode, ctx: CompilerContext) => string>;
306
+ protected readonly functionStrategy: FunctionStrategy;
307
+ protected readonly tableFunctionStrategy: TableFunctionStrategy;
308
+
309
+ protected constructor(functionStrategy?: FunctionStrategy, tableFunctionStrategy?: TableFunctionStrategy) {
310
+ this.expressionCompilers = new Map();
311
+ this.operandCompilers = new Map();
312
+ this.functionStrategy = functionStrategy || new StandardFunctionStrategy();
313
+ this.tableFunctionStrategy = tableFunctionStrategy || new StandardTableFunctionStrategy();
314
+ this.registerDefaultOperandCompilers();
315
+ this.registerDefaultExpressionCompilers();
316
+ }
317
+
318
+ /**
319
+ * Creates a new Dialect instance (for testing purposes)
320
+ * @param functionStrategy - Optional function strategy
321
+ * @returns New Dialect instance
322
+ */
323
+ static create(functionStrategy?: FunctionStrategy, tableFunctionStrategy?: TableFunctionStrategy): Dialect {
324
+ // Create a minimal concrete implementation for testing
325
+ class TestDialect extends Dialect {
326
+ protected readonly dialect: DialectName = 'sqlite';
327
+ quoteIdentifier(id: string): string {
328
+ return `"${id}"`;
329
+ }
330
+ protected compileSelectAst(): never {
331
+ throw new Error('Not implemented');
332
+ }
333
+ protected compileInsertAst(): never {
334
+ throw new Error('Not implemented');
335
+ }
336
+ protected compileUpdateAst(): never {
337
+ throw new Error('Not implemented');
338
+ }
329
339
  protected compileDeleteAst(): never {
330
340
  throw new Error('Not implemented');
331
341
  }
332
- }
333
- return new TestDialect(functionStrategy, tableFunctionStrategy);
334
- }
335
-
336
- /**
337
- * Registers an expression compiler for a specific node type
338
- * @param type - Expression node type
339
- * @param compiler - Compiler function
340
- */
341
- protected registerExpressionCompiler<T extends ExpressionNode>(type: T['type'], compiler: (node: T, ctx: CompilerContext) => string): void {
342
- this.expressionCompilers.set(type, compiler as (node: ExpressionNode, ctx: CompilerContext) => string);
343
- }
344
-
345
- /**
346
- * Registers an operand compiler for a specific node type
347
- * @param type - Operand node type
348
- * @param compiler - Compiler function
349
- */
350
- protected registerOperandCompiler<T extends OperandNode>(type: T['type'], compiler: (node: T, ctx: CompilerContext) => string): void {
351
- this.operandCompilers.set(type, compiler as (node: OperandNode, ctx: CompilerContext) => string);
352
- }
353
-
354
- /**
355
- * Compiles an expression node
356
- * @param node - Expression node to compile
357
- * @param ctx - Compiler context
358
- * @returns Compiled SQL expression
359
- */
360
- protected compileExpression(node: ExpressionNode, ctx: CompilerContext): string {
361
- const compiler = this.expressionCompilers.get(node.type);
362
- if (!compiler) {
363
- throw new Error(`Unsupported expression node type "${node.type}" for ${this.constructor.name}`);
364
- }
365
- return compiler(node, ctx);
366
- }
367
-
368
- /**
369
- * Compiles an operand node
370
- * @param node - Operand node to compile
371
- * @param ctx - Compiler context
372
- * @returns Compiled SQL operand
373
- */
374
- protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
375
- const compiler = this.operandCompilers.get(node.type);
376
- if (!compiler) {
377
- throw new Error(`Unsupported operand node type "${node.type}" for ${this.constructor.name}`);
378
- }
379
- return compiler(node, ctx);
380
- }
381
-
382
- /**
383
- * Compiles an ordering term (operand, expression, or alias reference).
384
- */
385
- protected compileOrderingTerm(term: OrderingTerm, ctx: CompilerContext): string {
386
- if (isOperandNode(term)) {
387
- return this.compileOperand(term, ctx);
388
- }
389
- // At this point, term must be an ExpressionNode
390
- const expr = this.compileExpression(term as ExpressionNode, ctx);
391
- return `(${expr})`;
392
- }
393
-
394
- private registerDefaultExpressionCompilers(): void {
395
- this.registerExpressionCompiler('BinaryExpression', (binary: BinaryExpressionNode, ctx) => {
396
- const left = this.compileOperand(binary.left, ctx);
397
- const right = this.compileOperand(binary.right, ctx);
398
- const base = `${left} ${binary.operator} ${right}`;
399
- if (binary.escape) {
400
- const escapeOperand = this.compileOperand(binary.escape, ctx);
401
- return `${base} ESCAPE ${escapeOperand}`;
402
- }
403
- return base;
404
- });
405
-
406
- this.registerExpressionCompiler('LogicalExpression', (logical: LogicalExpressionNode, ctx) => {
407
- if (logical.operands.length === 0) return '';
408
- const parts = logical.operands.map(op => {
409
- const compiled = this.compileExpression(op, ctx);
410
- return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
411
- });
412
- return parts.join(` ${logical.operator} `);
413
- });
414
-
415
- this.registerExpressionCompiler('NullExpression', (nullExpr: NullExpressionNode, ctx) => {
416
- const left = this.compileOperand(nullExpr.left, ctx);
417
- return `${left} ${nullExpr.operator}`;
418
- });
419
-
420
- this.registerExpressionCompiler('InExpression', (inExpr: InExpressionNode, ctx) => {
421
- const left = this.compileOperand(inExpr.left, ctx);
422
- if (Array.isArray(inExpr.right)) {
423
- const values = inExpr.right.map(v => this.compileOperand(v, ctx)).join(', ');
424
- return `${left} ${inExpr.operator} (${values})`;
425
- }
426
- const subquerySql = this.compileSelectAst(inExpr.right.query, ctx).trim().replace(/;$/, '');
427
- return `${left} ${inExpr.operator} (${subquerySql})`;
428
- });
429
-
430
- this.registerExpressionCompiler('ExistsExpression', (existsExpr: ExistsExpressionNode, ctx) => {
431
- const subquerySql = this.compileSelectForExists(existsExpr.subquery, ctx);
432
- return `${existsExpr.operator} (${subquerySql})`;
433
- });
434
-
435
- this.registerExpressionCompiler('BetweenExpression', (betweenExpr: BetweenExpressionNode, ctx) => {
436
- const left = this.compileOperand(betweenExpr.left, ctx);
437
- const lower = this.compileOperand(betweenExpr.lower, ctx);
438
- const upper = this.compileOperand(betweenExpr.upper, ctx);
439
- return `${left} ${betweenExpr.operator} ${lower} AND ${upper}`;
440
- });
441
-
442
- this.registerExpressionCompiler('ArithmeticExpression', (arith: ArithmeticExpressionNode, ctx) => {
443
- const left = this.compileOperand(arith.left, ctx);
444
- const right = this.compileOperand(arith.right, ctx);
445
- return `${left} ${arith.operator} ${right}`;
446
- });
447
-
448
- this.registerExpressionCompiler('BitwiseExpression', (bitwise: BitwiseExpressionNode, ctx) => {
449
- const left = this.compileOperand(bitwise.left, ctx);
450
- const right = this.compileOperand(bitwise.right, ctx);
451
- return `${left} ${bitwise.operator} ${right}`;
452
- });
453
- }
454
-
455
- private registerDefaultOperandCompilers(): void {
456
- this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
457
-
458
- this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
459
- void _ctx;
460
- return this.quoteIdentifier(alias.name);
461
- });
462
-
463
- this.registerOperandCompiler('Column', (column: ColumnNode, _ctx) => {
464
- void _ctx;
465
- return `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`;
466
- });
467
- this.registerOperandCompiler('Function', (fnNode: FunctionNode, ctx) =>
468
- this.compileFunctionOperand(fnNode, ctx)
469
- );
470
- this.registerOperandCompiler('JsonPath', (path: JsonPathNode, _ctx) => {
471
- void _ctx;
472
- return this.compileJsonPath(path);
473
- });
474
-
475
- this.registerOperandCompiler('ScalarSubquery', (node: ScalarSubqueryNode, ctx) => {
476
- const sql = this.compileSelectAst(node.query, ctx).trim().replace(/;$/, '');
477
- return `(${sql})`;
478
- });
479
-
480
- this.registerOperandCompiler('CaseExpression', (node: CaseExpressionNode, ctx) => {
481
- const parts = ['CASE'];
482
- for (const { when, then } of node.conditions) {
483
- parts.push(`WHEN ${this.compileExpression(when, ctx)} THEN ${this.compileOperand(then, ctx)}`);
484
- }
485
- if (node.else) {
486
- parts.push(`ELSE ${this.compileOperand(node.else, ctx)}`);
487
- }
488
- parts.push('END');
489
- return parts.join(' ');
490
- });
491
-
492
- this.registerOperandCompiler('Cast', (node: CastExpressionNode, ctx) => {
493
- const value = this.compileOperand(node.expression, ctx);
494
- return `CAST(${value} AS ${node.castType})`;
495
- });
496
-
497
- this.registerOperandCompiler('WindowFunction', (node: WindowFunctionNode, ctx) => {
498
- let result = `${node.name}(`;
499
- if (node.args.length > 0) {
500
- result += node.args.map(arg => this.compileOperand(arg, ctx)).join(', ');
501
- }
502
- result += ') OVER (';
503
-
504
- const parts: string[] = [];
505
-
506
- if (node.partitionBy && node.partitionBy.length > 0) {
507
- const partitionClause = 'PARTITION BY ' + node.partitionBy.map(col =>
508
- `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`
509
- ).join(', ');
510
- parts.push(partitionClause);
511
- }
512
-
513
- if (node.orderBy && node.orderBy.length > 0) {
514
- const orderClause = 'ORDER BY ' + node.orderBy.map(o => {
515
- const term = this.compileOrderingTerm(o.term, ctx);
516
- const collation = o.collation ? ` COLLATE ${o.collation}` : '';
517
- const nulls = o.nulls ? ` NULLS ${o.nulls}` : '';
518
- return `${term} ${o.direction}${collation}${nulls}`;
519
- }).join(', ');
520
- parts.push(orderClause);
342
+ compileProcedureCall(): CompiledProcedureCall {
343
+ throw new Error('Not implemented');
521
344
  }
522
-
523
- result += parts.join(' ');
524
- result += ')';
525
-
526
- return result;
527
- });
528
- this.registerOperandCompiler('ArithmeticExpression', (node: ArithmeticExpressionNode, ctx) => {
529
- const left = this.compileOperand(node.left, ctx);
530
- const right = this.compileOperand(node.right, ctx);
531
- return `(${left} ${node.operator} ${right})`;
532
- });
533
- this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
534
- const left = this.compileOperand(node.left, ctx);
535
- const right = this.compileOperand(node.right, ctx);
536
- return `(${left} ${node.operator} ${right})`;
537
- });
538
- this.registerOperandCompiler('Collate', (node: CollateExpressionNode, ctx) => {
539
- const expr = this.compileOperand(node.expression, ctx);
540
- return `${expr} COLLATE ${node.collation}`;
541
- });
542
- }
543
-
544
- // Default fallback, should be overridden by dialects if supported
545
- protected compileJsonPath(_node: JsonPathNode): string {
546
- void _node;
547
- throw new Error("JSON Path not supported by this dialect");
548
- }
549
-
550
- /**
551
- * Compiles a function operand, using the dialect's function strategy.
552
- */
553
- protected compileFunctionOperand(fnNode: FunctionNode, ctx: CompilerContext): string {
554
- const compiledArgs = fnNode.args.map(arg => this.compileOperand(arg, ctx));
555
- const renderer = this.functionStrategy.getRenderer(fnNode.name);
556
- if (renderer) {
557
- return renderer({
558
- node: fnNode,
559
- compiledArgs,
560
- compileOperand: operand => this.compileOperand(operand, ctx)
561
- });
562
345
  }
563
- return `${fnNode.name}(${compiledArgs.join(', ')})`;
564
- }
565
- }
346
+ return new TestDialect(functionStrategy, tableFunctionStrategy);
347
+ }
348
+
349
+ /**
350
+ * Registers an expression compiler for a specific node type
351
+ * @param type - Expression node type
352
+ * @param compiler - Compiler function
353
+ */
354
+ protected registerExpressionCompiler<T extends ExpressionNode>(type: T['type'], compiler: (node: T, ctx: CompilerContext) => string): void {
355
+ this.expressionCompilers.set(type, compiler as (node: ExpressionNode, ctx: CompilerContext) => string);
356
+ }
357
+
358
+ /**
359
+ * Registers an operand compiler for a specific node type
360
+ * @param type - Operand node type
361
+ * @param compiler - Compiler function
362
+ */
363
+ protected registerOperandCompiler<T extends OperandNode>(type: T['type'], compiler: (node: T, ctx: CompilerContext) => string): void {
364
+ this.operandCompilers.set(type, compiler as (node: OperandNode, ctx: CompilerContext) => string);
365
+ }
366
+
367
+ /**
368
+ * Compiles an expression node
369
+ * @param node - Expression node to compile
370
+ * @param ctx - Compiler context
371
+ * @returns Compiled SQL expression
372
+ */
373
+ protected compileExpression(node: ExpressionNode, ctx: CompilerContext): string {
374
+ const compiler = this.expressionCompilers.get(node.type);
375
+ if (!compiler) {
376
+ throw new Error(`Unsupported expression node type "${node.type}" for ${this.constructor.name}`);
377
+ }
378
+ return compiler(node, ctx);
379
+ }
380
+
381
+ /**
382
+ * Compiles an operand node
383
+ * @param node - Operand node to compile
384
+ * @param ctx - Compiler context
385
+ * @returns Compiled SQL operand
386
+ */
387
+ protected compileOperand(node: OperandNode, ctx: CompilerContext): string {
388
+ const compiler = this.operandCompilers.get(node.type);
389
+ if (!compiler) {
390
+ throw new Error(`Unsupported operand node type "${node.type}" for ${this.constructor.name}`);
391
+ }
392
+ return compiler(node, ctx);
393
+ }
394
+
395
+ /**
396
+ * Compiles an ordering term (operand, expression, or alias reference).
397
+ */
398
+ protected compileOrderingTerm(term: OrderingTerm, ctx: CompilerContext): string {
399
+ if (isOperandNode(term)) {
400
+ return this.compileOperand(term, ctx);
401
+ }
402
+ // At this point, term must be an ExpressionNode
403
+ const expr = this.compileExpression(term as ExpressionNode, ctx);
404
+ return `(${expr})`;
405
+ }
406
+
407
+ private registerDefaultExpressionCompilers(): void {
408
+ this.registerExpressionCompiler('BinaryExpression', (binary: BinaryExpressionNode, ctx) => {
409
+ const left = this.compileOperand(binary.left, ctx);
410
+ const right = this.compileOperand(binary.right, ctx);
411
+ const base = `${left} ${binary.operator} ${right}`;
412
+ if (binary.escape) {
413
+ const escapeOperand = this.compileOperand(binary.escape, ctx);
414
+ return `${base} ESCAPE ${escapeOperand}`;
415
+ }
416
+ return base;
417
+ });
418
+
419
+ this.registerExpressionCompiler('LogicalExpression', (logical: LogicalExpressionNode, ctx) => {
420
+ if (logical.operands.length === 0) return '';
421
+ const parts = logical.operands.map(op => {
422
+ const compiled = this.compileExpression(op, ctx);
423
+ return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
424
+ });
425
+ return parts.join(` ${logical.operator} `);
426
+ });
427
+
428
+ this.registerExpressionCompiler('NullExpression', (nullExpr: NullExpressionNode, ctx) => {
429
+ const left = this.compileOperand(nullExpr.left, ctx);
430
+ return `${left} ${nullExpr.operator}`;
431
+ });
432
+
433
+ this.registerExpressionCompiler('InExpression', (inExpr: InExpressionNode, ctx) => {
434
+ const left = this.compileOperand(inExpr.left, ctx);
435
+ if (Array.isArray(inExpr.right)) {
436
+ const values = inExpr.right.map(v => this.compileOperand(v, ctx)).join(', ');
437
+ return `${left} ${inExpr.operator} (${values})`;
438
+ }
439
+ const subquerySql = this.compileSelectAst(inExpr.right.query, ctx).trim().replace(/;$/, '');
440
+ return `${left} ${inExpr.operator} (${subquerySql})`;
441
+ });
442
+
443
+ this.registerExpressionCompiler('ExistsExpression', (existsExpr: ExistsExpressionNode, ctx) => {
444
+ const subquerySql = this.compileSelectForExists(existsExpr.subquery, ctx);
445
+ return `${existsExpr.operator} (${subquerySql})`;
446
+ });
447
+
448
+ this.registerExpressionCompiler('BetweenExpression', (betweenExpr: BetweenExpressionNode, ctx) => {
449
+ const left = this.compileOperand(betweenExpr.left, ctx);
450
+ const lower = this.compileOperand(betweenExpr.lower, ctx);
451
+ const upper = this.compileOperand(betweenExpr.upper, ctx);
452
+ return `${left} ${betweenExpr.operator} ${lower} AND ${upper}`;
453
+ });
454
+
455
+ this.registerExpressionCompiler('ArithmeticExpression', (arith: ArithmeticExpressionNode, ctx) => {
456
+ const left = this.compileOperand(arith.left, ctx);
457
+ const right = this.compileOperand(arith.right, ctx);
458
+ return `${left} ${arith.operator} ${right}`;
459
+ });
460
+
461
+ this.registerExpressionCompiler('BitwiseExpression', (bitwise: BitwiseExpressionNode, ctx) => {
462
+ const left = this.compileOperand(bitwise.left, ctx);
463
+ const right = this.compileOperand(bitwise.right, ctx);
464
+ return `${left} ${bitwise.operator} ${right}`;
465
+ });
466
+ }
467
+
468
+ private registerDefaultOperandCompilers(): void {
469
+ this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
470
+
471
+ this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
472
+ void _ctx;
473
+ return this.quoteIdentifier(alias.name);
474
+ });
475
+
476
+ this.registerOperandCompiler('Column', (column: ColumnNode, _ctx) => {
477
+ void _ctx;
478
+ return `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`;
479
+ });
480
+ this.registerOperandCompiler('Function', (fnNode: FunctionNode, ctx) =>
481
+ this.compileFunctionOperand(fnNode, ctx)
482
+ );
483
+ this.registerOperandCompiler('JsonPath', (path: JsonPathNode, _ctx) => {
484
+ void _ctx;
485
+ return this.compileJsonPath(path);
486
+ });
487
+
488
+ this.registerOperandCompiler('ScalarSubquery', (node: ScalarSubqueryNode, ctx) => {
489
+ const sql = this.compileSelectAst(node.query, ctx).trim().replace(/;$/, '');
490
+ return `(${sql})`;
491
+ });
492
+
493
+ this.registerOperandCompiler('CaseExpression', (node: CaseExpressionNode, ctx) => {
494
+ const parts = ['CASE'];
495
+ for (const { when, then } of node.conditions) {
496
+ parts.push(`WHEN ${this.compileExpression(when, ctx)} THEN ${this.compileOperand(then, ctx)}`);
497
+ }
498
+ if (node.else) {
499
+ parts.push(`ELSE ${this.compileOperand(node.else, ctx)}`);
500
+ }
501
+ parts.push('END');
502
+ return parts.join(' ');
503
+ });
504
+
505
+ this.registerOperandCompiler('Cast', (node: CastExpressionNode, ctx) => {
506
+ const value = this.compileOperand(node.expression, ctx);
507
+ return `CAST(${value} AS ${node.castType})`;
508
+ });
509
+
510
+ this.registerOperandCompiler('WindowFunction', (node: WindowFunctionNode, ctx) => {
511
+ let result = `${node.name}(`;
512
+ if (node.args.length > 0) {
513
+ result += node.args.map(arg => this.compileOperand(arg, ctx)).join(', ');
514
+ }
515
+ result += ') OVER (';
516
+
517
+ const parts: string[] = [];
518
+
519
+ if (node.partitionBy && node.partitionBy.length > 0) {
520
+ const partitionClause = 'PARTITION BY ' + node.partitionBy.map(col =>
521
+ `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`
522
+ ).join(', ');
523
+ parts.push(partitionClause);
524
+ }
525
+
526
+ if (node.orderBy && node.orderBy.length > 0) {
527
+ const orderClause = 'ORDER BY ' + node.orderBy.map(o => {
528
+ const term = this.compileOrderingTerm(o.term, ctx);
529
+ const collation = o.collation ? ` COLLATE ${o.collation}` : '';
530
+ const nulls = o.nulls ? ` NULLS ${o.nulls}` : '';
531
+ return `${term} ${o.direction}${collation}${nulls}`;
532
+ }).join(', ');
533
+ parts.push(orderClause);
534
+ }
535
+
536
+ result += parts.join(' ');
537
+ result += ')';
538
+
539
+ return result;
540
+ });
541
+ this.registerOperandCompiler('ArithmeticExpression', (node: ArithmeticExpressionNode, ctx) => {
542
+ const left = this.compileOperand(node.left, ctx);
543
+ const right = this.compileOperand(node.right, ctx);
544
+ return `(${left} ${node.operator} ${right})`;
545
+ });
546
+ this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
547
+ const left = this.compileOperand(node.left, ctx);
548
+ const right = this.compileOperand(node.right, ctx);
549
+ return `(${left} ${node.operator} ${right})`;
550
+ });
551
+ this.registerOperandCompiler('Collate', (node: CollateExpressionNode, ctx) => {
552
+ const expr = this.compileOperand(node.expression, ctx);
553
+ return `${expr} COLLATE ${node.collation}`;
554
+ });
555
+ }
556
+
557
+ // Default fallback, should be overridden by dialects if supported
558
+ protected compileJsonPath(_node: JsonPathNode): string {
559
+ void _node;
560
+ throw new Error("JSON Path not supported by this dialect");
561
+ }
562
+
563
+ /**
564
+ * Compiles a function operand, using the dialect's function strategy.
565
+ */
566
+ protected compileFunctionOperand(fnNode: FunctionNode, ctx: CompilerContext): string {
567
+ const compiledArgs = fnNode.args.map(arg => this.compileOperand(arg, ctx));
568
+ const renderer = this.functionStrategy.getRenderer(fnNode.name);
569
+ if (renderer) {
570
+ return renderer({
571
+ node: fnNode,
572
+ compiledArgs,
573
+ compileOperand: operand => this.compileOperand(operand, ctx)
574
+ });
575
+ }
576
+ return `${fnNode.name}(${compiledArgs.join(', ')})`;
577
+ }
578
+ }