metal-orm 1.1.22 → 1.1.23

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