metal-orm 1.0.39 → 1.0.41

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