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/dist/index.cjs +267 -400
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -151
- package/dist/index.d.ts +95 -151
- package/dist/index.js +262 -400
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/dialect/abstract.ts +94 -417
- package/src/core/dialect/base/expression-compiler-registry.ts +254 -0
- package/src/core/dialect/base/function-table-formatter.ts +40 -78
- package/src/core/dialect/base/select-ast-normalizer.ts +61 -0
- package/src/core/dialect/base/sql-dialect.ts +36 -59
- package/src/core/dialect/capabilities/procedure-compiler.ts +30 -0
- package/src/core/dialect/dialect-factory.ts +3 -4
- package/src/core/dialect/mssql/index.ts +3 -2
- package/src/core/dialect/mysql/index.ts +3 -2
- package/src/core/dialect/postgres/index.ts +3 -2
- package/src/core/dialect/sqlite/index.ts +1 -7
- package/src/index.ts +4 -1
- package/src/orm/execute-procedure.ts +3 -2
- package/src/query-builder/procedure-call.ts +4 -18
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|