metal-orm 1.0.13 → 1.0.15

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 (115) hide show
  1. package/README.md +75 -82
  2. package/dist/decorators/index.cjs +1600 -27
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +6 -2
  5. package/dist/decorators/index.d.ts +6 -2
  6. package/dist/decorators/index.js +1599 -27
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +4608 -3429
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +511 -159
  11. package/dist/index.d.ts +511 -159
  12. package/dist/index.js +4526 -3415
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-CCp1oz9p.d.cts → select-Bkv8g8u_.d.cts} +193 -67
  15. package/dist/{select-CCp1oz9p.d.ts → select-Bkv8g8u_.d.ts} +193 -67
  16. package/package.json +1 -1
  17. package/src/codegen/typescript.ts +38 -35
  18. package/src/core/ast/adapters.ts +21 -0
  19. package/src/core/ast/aggregate-functions.ts +13 -13
  20. package/src/core/ast/builders.ts +56 -43
  21. package/src/core/ast/expression-builders.ts +34 -34
  22. package/src/core/ast/expression-nodes.ts +18 -16
  23. package/src/core/ast/expression-visitor.ts +122 -69
  24. package/src/core/ast/expression.ts +6 -4
  25. package/src/core/ast/join-metadata.ts +15 -0
  26. package/src/core/ast/join-node.ts +22 -20
  27. package/src/core/ast/join.ts +5 -5
  28. package/src/core/ast/query.ts +52 -88
  29. package/src/core/ast/types.ts +20 -0
  30. package/src/core/ast/window-functions.ts +55 -55
  31. package/src/core/ddl/dialects/base-schema-dialect.ts +20 -6
  32. package/src/core/ddl/dialects/mssql-schema-dialect.ts +32 -8
  33. package/src/core/ddl/dialects/mysql-schema-dialect.ts +21 -10
  34. package/src/core/ddl/dialects/postgres-schema-dialect.ts +52 -7
  35. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +23 -9
  36. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  37. package/src/core/ddl/introspect/catalogs/postgres.ts +143 -0
  38. package/src/core/ddl/introspect/context.ts +9 -0
  39. package/src/core/ddl/introspect/functions/postgres.ts +26 -0
  40. package/src/core/ddl/introspect/mssql.ts +149 -149
  41. package/src/core/ddl/introspect/mysql.ts +99 -99
  42. package/src/core/ddl/introspect/postgres.ts +245 -154
  43. package/src/core/ddl/introspect/registry.ts +26 -0
  44. package/src/core/ddl/introspect/run-select.ts +25 -0
  45. package/src/core/ddl/introspect/sqlite.ts +7 -7
  46. package/src/core/ddl/introspect/types.ts +23 -19
  47. package/src/core/ddl/introspect/utils.ts +1 -1
  48. package/src/core/ddl/naming-strategy.ts +10 -0
  49. package/src/core/ddl/schema-dialect.ts +41 -0
  50. package/src/core/ddl/schema-diff.ts +211 -179
  51. package/src/core/ddl/schema-generator.ts +16 -90
  52. package/src/core/ddl/schema-introspect.ts +25 -32
  53. package/src/core/ddl/schema-plan-executor.ts +17 -0
  54. package/src/core/ddl/schema-types.ts +46 -39
  55. package/src/core/ddl/sql-writing.ts +170 -0
  56. package/src/core/dialect/abstract.ts +144 -126
  57. package/src/core/dialect/base/cte-compiler.ts +33 -0
  58. package/src/core/dialect/base/function-table-formatter.ts +132 -0
  59. package/src/core/dialect/base/groupby-compiler.ts +21 -0
  60. package/src/core/dialect/base/join-compiler.ts +26 -0
  61. package/src/core/dialect/base/orderby-compiler.ts +21 -0
  62. package/src/core/dialect/base/pagination-strategy.ts +32 -0
  63. package/src/core/dialect/base/returning-strategy.ts +56 -0
  64. package/src/core/dialect/base/sql-dialect.ts +181 -204
  65. package/src/core/dialect/dialect-factory.ts +91 -0
  66. package/src/core/dialect/mssql/functions.ts +101 -0
  67. package/src/core/dialect/mssql/index.ts +128 -126
  68. package/src/core/dialect/mysql/functions.ts +101 -0
  69. package/src/core/dialect/mysql/index.ts +20 -18
  70. package/src/core/dialect/postgres/functions.ts +95 -0
  71. package/src/core/dialect/postgres/index.ts +30 -28
  72. package/src/core/dialect/sqlite/functions.ts +115 -0
  73. package/src/core/dialect/sqlite/index.ts +30 -28
  74. package/src/core/driver/database-driver.ts +11 -0
  75. package/src/core/driver/mssql-driver.ts +20 -0
  76. package/src/core/driver/mysql-driver.ts +20 -0
  77. package/src/core/driver/postgres-driver.ts +20 -0
  78. package/src/core/driver/sqlite-driver.ts +20 -0
  79. package/src/core/execution/db-executor.ts +63 -0
  80. package/src/core/execution/executors/mssql-executor.ts +39 -0
  81. package/src/core/execution/executors/mysql-executor.ts +47 -0
  82. package/src/core/execution/executors/postgres-executor.ts +32 -0
  83. package/src/core/execution/executors/sqlite-executor.ts +31 -0
  84. package/src/core/functions/datetime.ts +132 -0
  85. package/src/core/functions/numeric.ts +179 -0
  86. package/src/core/functions/standard-strategy.ts +47 -0
  87. package/src/core/functions/text.ts +147 -0
  88. package/src/core/functions/types.ts +18 -0
  89. package/src/core/hydration/types.ts +57 -0
  90. package/src/decorators/bootstrap.ts +10 -0
  91. package/src/decorators/relations.ts +15 -0
  92. package/src/index.ts +30 -19
  93. package/src/orm/entity-metadata.ts +7 -0
  94. package/src/orm/entity.ts +58 -27
  95. package/src/orm/hydration.ts +25 -17
  96. package/src/orm/lazy-batch.ts +46 -2
  97. package/src/orm/orm-context.ts +60 -60
  98. package/src/orm/query-logger.ts +1 -1
  99. package/src/orm/relation-change-processor.ts +43 -2
  100. package/src/orm/relations/has-one.ts +139 -0
  101. package/src/orm/transaction-runner.ts +1 -1
  102. package/src/orm/unit-of-work.ts +60 -60
  103. package/src/query-builder/delete.ts +22 -5
  104. package/src/query-builder/hydration-manager.ts +2 -1
  105. package/src/query-builder/hydration-planner.ts +8 -7
  106. package/src/query-builder/insert.ts +22 -5
  107. package/src/query-builder/relation-conditions.ts +9 -8
  108. package/src/query-builder/relation-service.ts +3 -2
  109. package/src/query-builder/select.ts +66 -61
  110. package/src/query-builder/update.ts +22 -5
  111. package/src/schema/column.ts +246 -246
  112. package/src/schema/relation.ts +35 -1
  113. package/src/schema/table.ts +28 -28
  114. package/src/schema/types.ts +41 -31
  115. package/src/orm/db-executor.ts +0 -11
@@ -1,39 +1,46 @@
1
- import { ForeignKeyReference } from '../../schema/column.js';
2
- import { IndexColumn } from '../../schema/table.js';
3
-
4
- export interface DatabaseColumn {
5
- name: string;
6
- type: string;
7
- notNull?: boolean;
8
- default?: unknown;
9
- autoIncrement?: boolean;
10
- generated?: 'always' | 'byDefault';
11
- unique?: boolean | string;
12
- references?: ForeignKeyReference;
13
- check?: string;
14
- }
15
-
16
- export interface DatabaseIndex {
17
- name: string;
18
- columns: IndexColumn[];
19
- unique?: boolean;
20
- where?: string;
21
- }
22
-
23
- export interface DatabaseCheck {
24
- name?: string;
25
- expression: string;
26
- }
27
-
28
- export interface DatabaseTable {
29
- name: string;
30
- schema?: string;
31
- columns: DatabaseColumn[];
32
- primaryKey?: string[];
33
- indexes?: DatabaseIndex[];
34
- checks?: DatabaseCheck[];
35
- }
36
-
37
- export interface DatabaseSchema {
38
- tables: DatabaseTable[];
39
- }
1
+ import { ForeignKeyReference } from '../../schema/column.js';
2
+ import { IndexColumn } from '../../schema/table.js';
3
+
4
+ export interface ColumnDiff {
5
+ typeChanged?: boolean;
6
+ nullabilityChanged?: boolean;
7
+ defaultChanged?: boolean;
8
+ autoIncrementChanged?: boolean;
9
+ }
10
+
11
+ export interface DatabaseColumn {
12
+ name: string;
13
+ type: string;
14
+ notNull?: boolean;
15
+ default?: unknown;
16
+ autoIncrement?: boolean;
17
+ generated?: 'always' | 'byDefault';
18
+ unique?: boolean | string;
19
+ references?: ForeignKeyReference;
20
+ check?: string;
21
+ }
22
+
23
+ export interface DatabaseIndex {
24
+ name: string;
25
+ columns: IndexColumn[];
26
+ unique?: boolean;
27
+ where?: string;
28
+ }
29
+
30
+ export interface DatabaseCheck {
31
+ name?: string;
32
+ expression: string;
33
+ }
34
+
35
+ export interface DatabaseTable {
36
+ name: string;
37
+ schema?: string;
38
+ columns: DatabaseColumn[];
39
+ primaryKey?: string[];
40
+ indexes?: DatabaseIndex[];
41
+ checks?: DatabaseCheck[];
42
+ }
43
+
44
+ export interface DatabaseSchema {
45
+ tables: DatabaseTable[];
46
+ }
@@ -0,0 +1,170 @@
1
+ import type { TableDef, IndexColumn } from '../../schema/table.js';
2
+ import type { RawDefaultValue } from '../../schema/column.js';
3
+
4
+ /**
5
+ * Minimal surface for anything that can quote identifiers.
6
+ * Implemented by SchemaDialect, runtime Dialect, etc.
7
+ */
8
+ export interface Quoter {
9
+ quoteIdentifier(id: string): string;
10
+ }
11
+
12
+ /**
13
+ * Escape a value to be safe inside a single-quoted SQL literal.
14
+ * Purely mechanical; no dialect knowledge.
15
+ */
16
+ export const escapeSqlString = (value: string): string =>
17
+ value.replace(/'/g, "''");
18
+
19
+ /**
20
+ * Narrow a value to the RawDefaultValue shape.
21
+ * This is domain-specific but dialect-agnostic.
22
+ */
23
+ export const isRawDefault = (value: unknown): value is RawDefaultValue =>
24
+ typeof value === 'object' &&
25
+ value !== null &&
26
+ 'raw' in value &&
27
+ typeof (value as RawDefaultValue).raw === 'string';
28
+
29
+ /**
30
+ * Abstraction for "how do I turn values into SQL literals".
31
+ * Implemented or configured by each dialect.
32
+ */
33
+ export interface LiteralFormatter {
34
+ formatLiteral(value: unknown): string;
35
+ }
36
+
37
+ /**
38
+ * Declarative options for building a LiteralFormatter.
39
+ * Dialects configure behavior by data, not by being hard-coded here.
40
+ */
41
+ export interface LiteralFormatOptions {
42
+ nullLiteral?: string; // default: 'NULL'
43
+ booleanTrue?: string; // default: 'TRUE'
44
+ booleanFalse?: string; // default: 'FALSE'
45
+
46
+ numberFormatter?: (value: number) => string;
47
+ dateFormatter?: (value: Date) => string;
48
+ stringWrapper?: (escaped: string) => string; // how to wrap an escaped string
49
+ jsonWrapper?: (escaped: string) => string; // how to wrap escaped JSON
50
+ }
51
+
52
+ /**
53
+ * Factory for a value-based LiteralFormatter that:
54
+ * - Handles type dispatch (null/number/boolean/date/string/object/raw)
55
+ * - Delegates representation choices to options
56
+ * - Knows nothing about concrete dialects
57
+ */
58
+ export const createLiteralFormatter = (
59
+ options: LiteralFormatOptions = {}
60
+ ): LiteralFormatter => {
61
+ const {
62
+ nullLiteral = 'NULL',
63
+ booleanTrue = 'TRUE',
64
+ booleanFalse = 'FALSE',
65
+
66
+ numberFormatter = (value: number): string =>
67
+ Number.isFinite(value) ? String(value) : nullLiteral,
68
+
69
+ dateFormatter = (value: Date): string =>
70
+ `'${escapeSqlString(value.toISOString())}'`,
71
+
72
+ stringWrapper = (escaped: string): string => `'${escaped}'`,
73
+ jsonWrapper = (escaped: string): string => `'${escaped}'`,
74
+ } = options;
75
+
76
+ const wrapString = stringWrapper;
77
+ const wrapJson = jsonWrapper;
78
+
79
+ const format = (value: unknown): string => {
80
+ // Domain rule: raw defaults bypass all formatting.
81
+ if (isRawDefault(value)) return value.raw;
82
+
83
+ if (value === null) return nullLiteral;
84
+
85
+ if (typeof value === 'number') {
86
+ return numberFormatter(value);
87
+ }
88
+
89
+ if (typeof value === 'boolean') {
90
+ return value ? booleanTrue : booleanFalse;
91
+ }
92
+
93
+ if (value instanceof Date) {
94
+ return dateFormatter(value);
95
+ }
96
+
97
+ if (typeof value === 'string') {
98
+ return wrapString(escapeSqlString(value));
99
+ }
100
+
101
+ // Fallback: serialize to JSON then treat as string.
102
+ return wrapJson(escapeSqlString(JSON.stringify(value)));
103
+ };
104
+
105
+ return {
106
+ formatLiteral: format,
107
+ };
108
+ };
109
+
110
+ /**
111
+ * Convenience wrapper if you prefer a functional style at call-sites.
112
+ */
113
+ export const formatLiteral = (
114
+ formatter: LiteralFormatter,
115
+ value: unknown
116
+ ): string => formatter.formatLiteral(value);
117
+
118
+ /**
119
+ * Quotes a possibly qualified identifier like "schema.table" or "db.schema.table"
120
+ * using a Quoter that knows how to quote a single segment.
121
+ */
122
+ export const quoteQualified = (quoter: Quoter, identifier: string): string => {
123
+ const parts = identifier.split('.');
124
+ return parts.map(part => quoter.quoteIdentifier(part)).join('.');
125
+ };
126
+
127
+ /**
128
+ * Renders index column list, including optional order / nulls, using the
129
+ * provided Quoter for identifier quoting.
130
+ */
131
+ export const renderIndexColumns = (
132
+ quoter: Quoter,
133
+ columns: (string | IndexColumn)[]
134
+ ): string =>
135
+ columns
136
+ .map(col => {
137
+ if (typeof col === 'string') {
138
+ return quoter.quoteIdentifier(col);
139
+ }
140
+
141
+ const parts: string[] = [quoter.quoteIdentifier(col.column)];
142
+
143
+ if (col.order) {
144
+ parts.push(col.order);
145
+ }
146
+
147
+ if (col.nulls) {
148
+ parts.push(`NULLS ${col.nulls}`);
149
+ }
150
+
151
+ return parts.join(' ');
152
+ })
153
+ .join(', ');
154
+
155
+ /**
156
+ * Resolves the primary key column names for a table, based purely on schema
157
+ * metadata. This is domain logic, but independent from any dialect.
158
+ */
159
+ export const resolvePrimaryKey = (table: TableDef): string[] => {
160
+ if (Array.isArray(table.primaryKey) && table.primaryKey.length > 0) {
161
+ return table.primaryKey;
162
+ }
163
+
164
+ const columns = Object.values(table.columns ?? {});
165
+
166
+ // `primary` / `name` are domain-level properties of ColumnDef.
167
+ return columns
168
+ .filter((col: any) => col.primary)
169
+ .map((col: any) => col.name);
170
+ };
@@ -1,11 +1,11 @@
1
- import {
2
- SelectQueryNode,
3
- InsertQueryNode,
4
- UpdateQueryNode,
5
- DeleteQueryNode,
6
- SetOperationKind,
7
- CommonTableExpressionNode
8
- } from '../ast/query.js';
1
+ import {
2
+ SelectQueryNode,
3
+ InsertQueryNode,
4
+ UpdateQueryNode,
5
+ DeleteQueryNode,
6
+ SetOperationKind,
7
+ CommonTableExpressionNode
8
+ } from '../ast/query.js';
9
9
  import {
10
10
  ExpressionNode,
11
11
  BinaryExpressionNode,
@@ -23,6 +23,9 @@ import {
23
23
  WindowFunctionNode,
24
24
  BetweenExpressionNode
25
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';
26
29
 
27
30
  /**
28
31
  * Context for SQL compilation with parameter management
@@ -63,23 +66,25 @@ export interface DeleteCompiler {
63
66
  /**
64
67
  * Abstract base class for SQL dialect implementations
65
68
  */
66
- export abstract class Dialect
67
- implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler
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
+
69
74
  /**
70
75
  * Compiles a SELECT query AST to SQL
71
76
  * @param ast - Query AST to compile
72
77
  * @returns Compiled query with SQL and parameters
73
78
  */
74
- compileSelect(ast: SelectQueryNode): CompiledQuery {
75
- const ctx = this.createCompilerContext();
76
- const normalized = this.normalizeSelectAst(ast);
77
- const rawSql = this.compileSelectAst(normalized, ctx).trim();
78
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
79
- return {
80
- sql,
81
- params: [...ctx.params]
82
- };
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
+ };
83
88
  }
84
89
 
85
90
  compileInsert(ast: InsertQueryNode): CompiledQuery {
@@ -102,19 +107,19 @@ export abstract class Dialect
102
107
  };
103
108
  }
104
109
 
105
- compileDelete(ast: DeleteQueryNode): CompiledQuery {
106
- const ctx = this.createCompilerContext();
107
- const rawSql = this.compileDeleteAst(ast, ctx).trim();
110
+ compileDelete(ast: DeleteQueryNode): CompiledQuery {
111
+ const ctx = this.createCompilerContext();
112
+ const rawSql = this.compileDeleteAst(ast, ctx).trim();
108
113
  const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
109
114
  return {
110
115
  sql,
111
116
  params: [...ctx.params]
112
- };
113
- }
114
-
115
- supportsReturning(): boolean {
116
- return false;
117
- }
117
+ };
118
+ }
119
+
120
+ supportsReturning(): boolean {
121
+ return false;
122
+ }
118
123
 
119
124
  /**
120
125
  * Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
@@ -122,7 +127,7 @@ export abstract class Dialect
122
127
  * @param ctx - Compiler context
123
128
  * @returns SQL string
124
129
  */
125
- protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
130
+ protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
126
131
 
127
132
  protected abstract compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string;
128
133
  protected abstract compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string;
@@ -161,23 +166,23 @@ export abstract class Dialect
161
166
  * Does not add ';' at the end
162
167
  * @param ast - Query AST
163
168
  * @param ctx - Compiler context
164
- * @returns SQL for EXISTS subquery
165
- */
166
- protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
167
- const normalized = this.normalizeSelectAst(ast);
168
- const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, '');
169
-
170
- // When the subquery is a set operation, wrap it as a derived table to keep valid syntax.
171
- if (normalized.setOps && normalized.setOps.length > 0) {
172
- return `SELECT 1 FROM (${full}) AS _exists`;
173
- }
174
-
175
- const upper = full.toUpperCase();
176
- const fromIndex = upper.indexOf(' FROM ');
177
- if (fromIndex === -1) {
178
- return full;
179
- }
180
-
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
+
181
186
  const tail = full.slice(fromIndex);
182
187
  return `SELECT 1${tail}`;
183
188
  }
@@ -204,82 +209,84 @@ export abstract class Dialect
204
209
  * @param index - Parameter index
205
210
  * @returns Formatted placeholder string
206
211
  */
207
- protected formatPlaceholder(index: number): string {
208
- return '?';
209
- }
210
-
211
- /**
212
- * Whether the current dialect supports a given set operation.
213
- * Override in concrete dialects to restrict support.
214
- */
215
- protected supportsSetOperation(kind: SetOperationKind): boolean {
216
- return true;
217
- }
218
-
219
- /**
220
- * Validates set-operation semantics:
221
- * - Ensures the dialect supports requested operators.
222
- * - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
223
- * @param ast - Query to validate
224
- * @param isOutermost - Whether this node is the outermost compound query
225
- */
226
- protected validateSetOperations(ast: SelectQueryNode, isOutermost = true): void {
227
- const hasSetOps = !!(ast.setOps && ast.setOps.length);
228
- if (!isOutermost && (ast.orderBy || ast.limit !== undefined || ast.offset !== undefined)) {
229
- throw new Error('ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.');
230
- }
231
-
232
- if (hasSetOps) {
233
- for (const op of ast.setOps!) {
234
- if (!this.supportsSetOperation(op.operator)) {
235
- throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
236
- }
237
- this.validateSetOperations(op.query, false);
238
- }
239
- }
240
- }
241
-
242
- /**
243
- * Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
244
- * @param ast - Query AST
245
- * @returns Normalized AST without inner CTEs and a list of hoisted CTEs
246
- */
247
- private hoistCtes(ast: SelectQueryNode): { normalized: SelectQueryNode; hoistedCtes: CommonTableExpressionNode[] } {
248
- let hoisted: CommonTableExpressionNode[] = [];
249
-
250
- const normalizedSetOps = ast.setOps?.map(op => {
251
- const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
252
- const childCtes = child.ctes ?? [];
253
- if (childCtes.length) {
254
- hoisted = hoisted.concat(childCtes);
255
- }
256
- hoisted = hoisted.concat(childHoisted);
257
- const queryWithoutCtes = childCtes.length ? { ...child, ctes: undefined } : child;
258
- return { ...op, query: queryWithoutCtes };
259
- });
260
-
261
- const normalized: SelectQueryNode = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
262
- return { normalized, hoistedCtes: hoisted };
263
- }
264
-
265
- /**
266
- * Normalizes a SELECT AST before compilation (validation + CTE hoisting).
267
- * @param ast - Query AST
268
- * @returns Normalized query AST
269
- */
270
- protected normalizeSelectAst(ast: SelectQueryNode): SelectQueryNode {
271
- this.validateSetOperations(ast, true);
272
- const { normalized, hoistedCtes } = this.hoistCtes(ast);
273
- const combinedCtes = [...(normalized.ctes ?? []), ...hoistedCtes];
274
- return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
275
- }
276
-
277
- private readonly expressionCompilers: Map<string, (node: ExpressionNode, ctx: CompilerContext) => string>;
278
- private readonly operandCompilers: Map<string, (node: OperandNode, ctx: CompilerContext) => string>;
279
-
280
- protected constructor() {
281
- this.expressionCompilers = new Map();
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();
282
288
  this.operandCompilers = new Map();
289
+ this.functionStrategy = functionStrategy || new StandardFunctionStrategy();
283
290
  this.registerDefaultOperandCompilers();
284
291
  this.registerDefaultExpressionCompilers();
285
292
  }
@@ -381,10 +388,9 @@ export abstract class Dialect
381
388
  this.registerOperandCompiler('Column', (column: ColumnNode, _ctx) => {
382
389
  return `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`;
383
390
  });
384
- this.registerOperandCompiler('Function', (fnNode: FunctionNode, ctx) => {
385
- const args = fnNode.args.map(arg => this.compileOperand(arg, ctx)).join(', ');
386
- return `${fnNode.name}(${args})`;
387
- });
391
+ this.registerOperandCompiler('Function', (fnNode: FunctionNode, ctx) =>
392
+ this.compileFunctionOperand(fnNode, ctx)
393
+ );
388
394
  this.registerOperandCompiler('JsonPath', (path: JsonPathNode, _ctx) => this.compileJsonPath(path));
389
395
 
390
396
  this.registerOperandCompiler('ScalarSubquery', (node: ScalarSubqueryNode, ctx) => {
@@ -438,4 +444,16 @@ export abstract class Dialect
438
444
  protected compileJsonPath(node: JsonPathNode): string {
439
445
  throw new Error("JSON Path not supported by this dialect");
440
446
  }
447
+
448
+ /**
449
+ * Compiles a function operand, using the dialect's function strategy.
450
+ */
451
+ protected compileFunctionOperand(fnNode: FunctionNode, ctx: CompilerContext): string {
452
+ const compiledArgs = fnNode.args.map(arg => this.compileOperand(arg, ctx));
453
+ const renderer = this.functionStrategy.getRenderer(fnNode.name);
454
+ if (renderer) {
455
+ return renderer({ node: fnNode, compiledArgs });
456
+ }
457
+ return `${fnNode.name}(${compiledArgs.join(', ')})`;
458
+ }
441
459
  }
@@ -0,0 +1,33 @@
1
+ import { SelectQueryNode } from '../../ast/query.js';
2
+ import { CompilerContext } from '../abstract.js';
3
+
4
+ /**
5
+ * Compiler for Common Table Expressions (CTEs).
6
+ * Handles compilation of WITH and WITH RECURSIVE clauses.
7
+ */
8
+ export class CteCompiler {
9
+ /**
10
+ * Compiles CTEs (WITH clauses) including recursive CTEs.
11
+ * @param ast - The SELECT query AST containing CTE definitions.
12
+ * @param ctx - The compiler context for expression compilation.
13
+ * @param quoteIdentifier - Function to quote identifiers according to dialect rules.
14
+ * @param compileSelectAst - Function to recursively compile SELECT query ASTs.
15
+ * @param normalizeSelectAst - Function to normalize SELECT query ASTs before compilation.
16
+ * @param stripTrailingSemicolon - Function to remove trailing semicolons from SQL.
17
+ * @returns SQL WITH clause string (e.g., "WITH cte_name AS (...) ") or empty string if no CTEs.
18
+ */
19
+ static compileCtes(ast: SelectQueryNode, ctx: CompilerContext, quoteIdentifier: (id: string) => string, compileSelectAst: (ast: SelectQueryNode, ctx: CompilerContext) => string, normalizeSelectAst: (ast: SelectQueryNode) => SelectQueryNode, stripTrailingSemicolon: (sql: string) => string): string {
20
+ if (!ast.ctes || ast.ctes.length === 0) return '';
21
+ const hasRecursive = ast.ctes.some(cte => cte.recursive);
22
+ const prefix = hasRecursive ? 'WITH RECURSIVE ' : 'WITH ';
23
+ const cteDefs = ast.ctes.map(cte => {
24
+ const name = quoteIdentifier(cte.name);
25
+ const cols = cte.columns && cte.columns.length
26
+ ? `(${cte.columns.map(c => quoteIdentifier(c)).join(', ')})`
27
+ : '';
28
+ const query = stripTrailingSemicolon(compileSelectAst(normalizeSelectAst(cte.query), ctx));
29
+ return `${name}${cols} AS (${query})`;
30
+ }).join(', ');
31
+ return `${prefix}${cteDefs} `;
32
+ }
33
+ }