metal-orm 1.0.55 → 1.0.57

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