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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { CompilerContext } from '../abstract.js';
|
|
2
|
+
import type { OrderingTerm, SelectQueryNode } from '../../ast/query.js';
|
|
3
|
+
import {
|
|
4
|
+
type ExpressionNode,
|
|
5
|
+
type BinaryExpressionNode,
|
|
6
|
+
type LogicalExpressionNode,
|
|
7
|
+
type NotExpressionNode,
|
|
8
|
+
type NullExpressionNode,
|
|
9
|
+
type InExpressionNode,
|
|
10
|
+
type ExistsExpressionNode,
|
|
11
|
+
type LiteralNode,
|
|
12
|
+
type ColumnNode,
|
|
13
|
+
type OperandNode,
|
|
14
|
+
type FunctionNode,
|
|
15
|
+
type JsonPathNode,
|
|
16
|
+
type ScalarSubqueryNode,
|
|
17
|
+
type CaseExpressionNode,
|
|
18
|
+
type CastExpressionNode,
|
|
19
|
+
type WindowFunctionNode,
|
|
20
|
+
type BetweenExpressionNode,
|
|
21
|
+
type ArithmeticExpressionNode,
|
|
22
|
+
type BitwiseExpressionNode,
|
|
23
|
+
type CollateExpressionNode,
|
|
24
|
+
type AliasRefNode,
|
|
25
|
+
type IsDistinctExpressionNode,
|
|
26
|
+
isOperandNode
|
|
27
|
+
} from '../../ast/expression.js';
|
|
28
|
+
|
|
29
|
+
export interface ExpressionCompilerHost {
|
|
30
|
+
quoteIdentifier(id: string): string;
|
|
31
|
+
compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
|
|
32
|
+
compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string;
|
|
33
|
+
compileJsonPath(node: JsonPathNode): string;
|
|
34
|
+
compileFunctionOperand(node: FunctionNode, ctx: CompilerContext): string;
|
|
35
|
+
describe(): string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ExpressionCompiler = (node: ExpressionNode, ctx: CompilerContext) => string;
|
|
39
|
+
type OperandCompiler = (node: OperandNode, ctx: CompilerContext) => string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Owns expression/operand dispatch independently from dialect query compilation.
|
|
43
|
+
* Dialects can replace individual node compilers without inheriting a second AST
|
|
44
|
+
* dispatcher or duplicating the default SQL expression behavior.
|
|
45
|
+
*/
|
|
46
|
+
export class ExpressionCompilerRegistry {
|
|
47
|
+
private readonly expressionCompilers = new Map<string, ExpressionCompiler>();
|
|
48
|
+
private readonly operandCompilers = new Map<string, OperandCompiler>();
|
|
49
|
+
|
|
50
|
+
constructor(private readonly host: ExpressionCompilerHost) {
|
|
51
|
+
this.registerDefaultOperandCompilers();
|
|
52
|
+
this.registerDefaultExpressionCompilers();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
registerExpressionCompiler<T extends ExpressionNode>(
|
|
56
|
+
type: T['type'],
|
|
57
|
+
compiler: (node: T, ctx: CompilerContext) => string
|
|
58
|
+
): void {
|
|
59
|
+
this.expressionCompilers.set(type, compiler as ExpressionCompiler);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
registerOperandCompiler<T extends OperandNode>(
|
|
63
|
+
type: T['type'],
|
|
64
|
+
compiler: (node: T, ctx: CompilerContext) => string
|
|
65
|
+
): void {
|
|
66
|
+
this.operandCompilers.set(type, compiler as OperandCompiler);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
compileExpression(node: ExpressionNode, ctx: CompilerContext): string {
|
|
70
|
+
const compiler = this.expressionCompilers.get(node.type);
|
|
71
|
+
if (!compiler) {
|
|
72
|
+
throw new Error(`Unsupported expression node type "${node.type}" for ${this.host.describe()}`);
|
|
73
|
+
}
|
|
74
|
+
return compiler(node, ctx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
compileOperand(node: OperandNode, ctx: CompilerContext): string {
|
|
78
|
+
const compiler = this.operandCompilers.get(node.type);
|
|
79
|
+
if (!compiler) {
|
|
80
|
+
throw new Error(`Unsupported operand node type "${node.type}" for ${this.host.describe()}`);
|
|
81
|
+
}
|
|
82
|
+
return compiler(node, ctx);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
compileOrderingTerm(term: OrderingTerm, ctx: CompilerContext): string {
|
|
86
|
+
if (isOperandNode(term)) {
|
|
87
|
+
return this.compileOperand(term, ctx);
|
|
88
|
+
}
|
|
89
|
+
return `(${this.compileExpression(term as ExpressionNode, ctx)})`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private registerDefaultExpressionCompilers(): void {
|
|
93
|
+
this.registerExpressionCompiler('BinaryExpression', (binary: BinaryExpressionNode, ctx) => {
|
|
94
|
+
const left = this.compileOperand(binary.left, ctx);
|
|
95
|
+
const right = this.compileOperand(binary.right, ctx);
|
|
96
|
+
const base = `${left} ${binary.operator} ${right}`;
|
|
97
|
+
if (binary.escape) {
|
|
98
|
+
const escapeOperand = this.compileOperand(binary.escape, ctx);
|
|
99
|
+
return `${base} ESCAPE ${escapeOperand}`;
|
|
100
|
+
}
|
|
101
|
+
return base;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.registerExpressionCompiler('LogicalExpression', (logical: LogicalExpressionNode, ctx) => {
|
|
105
|
+
if (logical.operands.length === 0) return '';
|
|
106
|
+
const parts = logical.operands.map(op => {
|
|
107
|
+
const compiled = this.compileExpression(op, ctx);
|
|
108
|
+
return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
|
|
109
|
+
});
|
|
110
|
+
return parts.join(` ${logical.operator} `);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.registerExpressionCompiler('NotExpression', (notExpr: NotExpressionNode, ctx) => {
|
|
114
|
+
const operand = this.compileExpression(notExpr.operand, ctx);
|
|
115
|
+
return `NOT (${operand})`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.registerExpressionCompiler('NullExpression', (nullExpr: NullExpressionNode, ctx) => {
|
|
119
|
+
const left = this.compileOperand(nullExpr.left, ctx);
|
|
120
|
+
return `${left} ${nullExpr.operator}`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.registerExpressionCompiler('InExpression', (inExpr: InExpressionNode, ctx) => {
|
|
124
|
+
const left = this.compileOperand(inExpr.left, ctx);
|
|
125
|
+
if (Array.isArray(inExpr.right)) {
|
|
126
|
+
const values = inExpr.right.map(v => this.compileOperand(v, ctx)).join(', ');
|
|
127
|
+
return `${left} ${inExpr.operator} (${values})`;
|
|
128
|
+
}
|
|
129
|
+
const subquerySql = this.host.compileSelectAst(inExpr.right.query, ctx).trim().replace(/;$/, '');
|
|
130
|
+
return `${left} ${inExpr.operator} (${subquerySql})`;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.registerExpressionCompiler('ExistsExpression', (existsExpr: ExistsExpressionNode, ctx) => {
|
|
134
|
+
const subquerySql = this.host.compileSelectForExists(existsExpr.subquery, ctx);
|
|
135
|
+
return `${existsExpr.operator} (${subquerySql})`;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.registerExpressionCompiler('BetweenExpression', (betweenExpr: BetweenExpressionNode, ctx) => {
|
|
139
|
+
const left = this.compileOperand(betweenExpr.left, ctx);
|
|
140
|
+
const lower = this.compileOperand(betweenExpr.lower, ctx);
|
|
141
|
+
const upper = this.compileOperand(betweenExpr.upper, ctx);
|
|
142
|
+
return `${left} ${betweenExpr.operator} ${lower} AND ${upper}`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.registerExpressionCompiler('ArithmeticExpression', (arith: ArithmeticExpressionNode, ctx) => {
|
|
146
|
+
const left = this.compileOperand(arith.left, ctx);
|
|
147
|
+
const right = this.compileOperand(arith.right, ctx);
|
|
148
|
+
return `${left} ${arith.operator} ${right}`;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.registerExpressionCompiler('BitwiseExpression', (bitwise: BitwiseExpressionNode, ctx) => {
|
|
152
|
+
const left = this.compileOperand(bitwise.left, ctx);
|
|
153
|
+
const right = this.compileOperand(bitwise.right, ctx);
|
|
154
|
+
return `${left} ${bitwise.operator} ${right}`;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.registerExpressionCompiler('IsDistinctExpression', (node: IsDistinctExpressionNode, ctx) => {
|
|
158
|
+
const left = this.compileOperand(node.left, ctx);
|
|
159
|
+
const right = this.compileOperand(node.right, ctx);
|
|
160
|
+
return `${left} ${node.operator} ${right}`;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private registerDefaultOperandCompilers(): void {
|
|
165
|
+
this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) =>
|
|
166
|
+
ctx.addParameter(literal.value)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
this.registerOperandCompiler('AliasRef', (alias: AliasRefNode) =>
|
|
170
|
+
this.host.quoteIdentifier(alias.name)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
this.registerOperandCompiler('Column', (column: ColumnNode) =>
|
|
174
|
+
`${this.host.quoteIdentifier(column.table)}.${this.host.quoteIdentifier(column.name)}`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
this.registerOperandCompiler('Function', (fnNode: FunctionNode, ctx) =>
|
|
178
|
+
this.host.compileFunctionOperand(fnNode, ctx)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
this.registerOperandCompiler('JsonPath', (path: JsonPathNode) =>
|
|
182
|
+
this.host.compileJsonPath(path)
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
this.registerOperandCompiler('ScalarSubquery', (node: ScalarSubqueryNode, ctx) => {
|
|
186
|
+
const sql = this.host.compileSelectAst(node.query, ctx).trim().replace(/;$/, '');
|
|
187
|
+
return `(${sql})`;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.registerOperandCompiler('CaseExpression', (node: CaseExpressionNode, ctx) => {
|
|
191
|
+
const parts = ['CASE'];
|
|
192
|
+
for (const { when, then } of node.conditions) {
|
|
193
|
+
parts.push(`WHEN ${this.compileExpression(when, ctx)} THEN ${this.compileOperand(then, ctx)}`);
|
|
194
|
+
}
|
|
195
|
+
if (node.else) {
|
|
196
|
+
parts.push(`ELSE ${this.compileOperand(node.else, ctx)}`);
|
|
197
|
+
}
|
|
198
|
+
parts.push('END');
|
|
199
|
+
return parts.join(' ');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
this.registerOperandCompiler('Cast', (node: CastExpressionNode, ctx) => {
|
|
203
|
+
const value = this.compileOperand(node.expression, ctx);
|
|
204
|
+
return `CAST(${value} AS ${node.castType})`;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.registerOperandCompiler('WindowFunction', (node: WindowFunctionNode, ctx) => {
|
|
208
|
+
let result = `${node.name}(`;
|
|
209
|
+
if (node.args.length > 0) {
|
|
210
|
+
result += node.args.map(arg => this.compileOperand(arg, ctx)).join(', ');
|
|
211
|
+
}
|
|
212
|
+
result += ') OVER (';
|
|
213
|
+
|
|
214
|
+
const parts: string[] = [];
|
|
215
|
+
if (node.partitionBy && node.partitionBy.length > 0) {
|
|
216
|
+
const partitionClause = 'PARTITION BY ' + node.partitionBy.map(col =>
|
|
217
|
+
`${this.host.quoteIdentifier(col.table)}.${this.host.quoteIdentifier(col.name)}`
|
|
218
|
+
).join(', ');
|
|
219
|
+
parts.push(partitionClause);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (node.orderBy && node.orderBy.length > 0) {
|
|
223
|
+
const orderClause = 'ORDER BY ' + node.orderBy.map(o => {
|
|
224
|
+
const term = this.compileOrderingTerm(o.term, ctx);
|
|
225
|
+
const collation = o.collation ? ` COLLATE ${o.collation}` : '';
|
|
226
|
+
const nulls = o.nulls ? ` NULLS ${o.nulls}` : '';
|
|
227
|
+
return `${term} ${o.direction}${collation}${nulls}`;
|
|
228
|
+
}).join(', ');
|
|
229
|
+
parts.push(orderClause);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
result += parts.join(' ');
|
|
233
|
+
result += ')';
|
|
234
|
+
return result;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.registerOperandCompiler('ArithmeticExpression', (node: ArithmeticExpressionNode, ctx) => {
|
|
238
|
+
const left = this.compileOperand(node.left, ctx);
|
|
239
|
+
const right = this.compileOperand(node.right, ctx);
|
|
240
|
+
return `(${left} ${node.operator} ${right})`;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
this.registerOperandCompiler('BitwiseExpression', (node: BitwiseExpressionNode, ctx) => {
|
|
244
|
+
const left = this.compileOperand(node.left, ctx);
|
|
245
|
+
const right = this.compileOperand(node.right, ctx);
|
|
246
|
+
return `(${left} ${node.operator} ${right})`;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
this.registerOperandCompiler('Collate', (node: CollateExpressionNode, ctx) => {
|
|
250
|
+
const expr = this.compileOperand(node.expression, ctx);
|
|
251
|
+
return `${expr} COLLATE ${node.collation}`;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -1,112 +1,74 @@
|
|
|
1
|
-
import { CompilerContext } from '../abstract.js';
|
|
2
|
-
import { OperandNode } from '../../ast/expression.js';
|
|
3
|
-
import { FunctionTableNode } from '../../ast/query.js';
|
|
4
|
-
|
|
1
|
+
import type { CompilerContext } from '../abstract.js';
|
|
2
|
+
import type { OperandNode } from '../../ast/expression.js';
|
|
3
|
+
import type { FunctionTableNode } from '../../ast/query.js';
|
|
4
|
+
|
|
5
|
+
export interface FunctionTableFormattingContext {
|
|
6
|
+
quoteIdentifier(id: string): string;
|
|
7
|
+
compileOperand(node: OperandNode, ctx: CompilerContext): string;
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
11
|
+
* Formats function-table expressions without depending on a dialect base class.
|
|
12
|
+
* SQL compilers provide only the two operations this formatter actually needs.
|
|
9
13
|
*/
|
|
10
14
|
export class FunctionTableFormatter {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
static format(fn: FunctionTableNode, ctx?: CompilerContext, dialect?: SqlDialectBase): string {
|
|
19
|
-
const schemaPart = this.formatSchema(fn, dialect);
|
|
20
|
-
const args = this.formatArgs(fn, ctx, dialect);
|
|
15
|
+
static format(
|
|
16
|
+
fn: FunctionTableNode,
|
|
17
|
+
ctx: CompilerContext | undefined,
|
|
18
|
+
formatter: FunctionTableFormattingContext
|
|
19
|
+
): string {
|
|
20
|
+
const schemaPart = this.formatSchema(fn, formatter);
|
|
21
|
+
const args = this.formatArgs(fn, ctx, formatter);
|
|
21
22
|
const base = this.formatBase(fn, schemaPart, args);
|
|
22
23
|
const lateral = this.formatLateral(fn);
|
|
23
|
-
const alias = this.formatAlias(fn,
|
|
24
|
-
const colAliases = this.formatColumnAliases(fn,
|
|
24
|
+
const alias = this.formatAlias(fn, formatter);
|
|
25
|
+
const colAliases = this.formatColumnAliases(fn, formatter);
|
|
25
26
|
return `${lateral}${base}${alias}${colAliases}`;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* @returns Schema prefix (e.g., "schema.") or empty string.
|
|
33
|
-
* @internal
|
|
34
|
-
*/
|
|
35
|
-
private static formatSchema(fn: FunctionTableNode, dialect?: SqlDialectBase): string {
|
|
29
|
+
private static formatSchema(
|
|
30
|
+
fn: FunctionTableNode,
|
|
31
|
+
formatter: FunctionTableFormattingContext
|
|
32
|
+
): string {
|
|
36
33
|
if (!fn.schema) return '';
|
|
37
|
-
|
|
38
|
-
return `${quoted}.`;
|
|
34
|
+
return `${formatter.quoteIdentifier(fn.schema)}.`;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* @returns Comma-separated function arguments.
|
|
47
|
-
* @internal
|
|
48
|
-
*/
|
|
49
|
-
private static formatArgs(fn: FunctionTableNode, ctx?: CompilerContext, dialect?: SqlDialectBase): string {
|
|
37
|
+
private static formatArgs(
|
|
38
|
+
fn: FunctionTableNode,
|
|
39
|
+
ctx: CompilerContext | undefined,
|
|
40
|
+
formatter: FunctionTableFormattingContext
|
|
41
|
+
): string {
|
|
50
42
|
return (fn.args || [])
|
|
51
|
-
.map((
|
|
52
|
-
if (ctx && dialect) {
|
|
53
|
-
return (dialect as unknown as { compileOperand(n: OperandNode, c: CompilerContext): string }).compileOperand(a, ctx);
|
|
54
|
-
}
|
|
55
|
-
return String(a);
|
|
56
|
-
})
|
|
43
|
+
.map((arg: OperandNode) => ctx ? formatter.compileOperand(arg, ctx) : String(arg))
|
|
57
44
|
.join(', ');
|
|
58
45
|
}
|
|
59
46
|
|
|
60
|
-
/**
|
|
61
|
-
* Formats the base function call with WITH ORDINALITY if present.
|
|
62
|
-
* @param fn - The function table node.
|
|
63
|
-
* @param schemaPart - Formatted schema prefix.
|
|
64
|
-
* @param args - Formatted function arguments.
|
|
65
|
-
* @param dialect - The dialect instance for quoting identifiers.
|
|
66
|
-
* @returns Base function call expression (e.g., "schema.func(args) WITH ORDINALITY").
|
|
67
|
-
* @internal
|
|
68
|
-
*/
|
|
69
47
|
private static formatBase(fn: FunctionTableNode, schemaPart: string, args: string): string {
|
|
70
48
|
const ordinality = fn.withOrdinality ? ' WITH ORDINALITY' : '';
|
|
71
49
|
return `${schemaPart}${fn.name}(${args})${ordinality}`;
|
|
72
50
|
}
|
|
73
51
|
|
|
74
|
-
/**
|
|
75
|
-
* Formats the LATERAL keyword if present.
|
|
76
|
-
* @param fn - The function table node.
|
|
77
|
-
* @returns "LATERAL " or empty string.
|
|
78
|
-
* @internal
|
|
79
|
-
*/
|
|
80
52
|
private static formatLateral(fn: FunctionTableNode): string {
|
|
81
53
|
return fn.lateral ? 'LATERAL ' : '';
|
|
82
54
|
}
|
|
83
55
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* @returns " AS alias" or empty string.
|
|
89
|
-
* @internal
|
|
90
|
-
*/
|
|
91
|
-
private static formatAlias(fn: FunctionTableNode, dialect?: SqlDialectBase): string {
|
|
56
|
+
private static formatAlias(
|
|
57
|
+
fn: FunctionTableNode,
|
|
58
|
+
formatter: FunctionTableFormattingContext
|
|
59
|
+
): string {
|
|
92
60
|
if (!fn.alias) return '';
|
|
93
|
-
|
|
94
|
-
return ` AS ${quoted}`;
|
|
61
|
+
return ` AS ${formatter.quoteIdentifier(fn.alias)}`;
|
|
95
62
|
}
|
|
96
63
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* @returns "(col1, col2, ...)" or empty string.
|
|
102
|
-
* @internal
|
|
103
|
-
*/
|
|
104
|
-
private static formatColumnAliases(fn: FunctionTableNode, dialect?: SqlDialectBase): string {
|
|
64
|
+
private static formatColumnAliases(
|
|
65
|
+
fn: FunctionTableNode,
|
|
66
|
+
formatter: FunctionTableFormattingContext
|
|
67
|
+
): string {
|
|
105
68
|
if (!fn.columnAliases || !fn.columnAliases.length) return '';
|
|
106
69
|
const aliases = fn.columnAliases
|
|
107
|
-
.map(col =>
|
|
70
|
+
.map(col => formatter.quoteIdentifier(col))
|
|
108
71
|
.join(', ');
|
|
109
72
|
return `(${aliases})`;
|
|
110
73
|
}
|
|
111
74
|
}
|
|
112
|
-
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommonTableExpressionNode,
|
|
3
|
+
SelectQueryNode,
|
|
4
|
+
SetOperationKind
|
|
5
|
+
} from '../../ast/query.js';
|
|
6
|
+
|
|
7
|
+
export type SetOperationSupport = (kind: SetOperationKind) => boolean;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes compound SELECT ASTs independently from SQL rendering.
|
|
11
|
+
*
|
|
12
|
+
* Responsibilities are intentionally limited to set-operation validation and
|
|
13
|
+
* CTE hoisting, so dialect SQL compilers consume one stable normalized shape.
|
|
14
|
+
*/
|
|
15
|
+
export class SelectAstNormalizer {
|
|
16
|
+
constructor(private readonly supportsSetOperation: SetOperationSupport) {}
|
|
17
|
+
|
|
18
|
+
normalize(ast: SelectQueryNode): SelectQueryNode {
|
|
19
|
+
this.validateSetOperations(ast, true);
|
|
20
|
+
const { normalized, hoistedCtes } = this.hoistCtes(ast);
|
|
21
|
+
const combinedCtes = [...(normalized.ctes ?? []), ...hoistedCtes];
|
|
22
|
+
return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private validateSetOperations(ast: SelectQueryNode, isOutermost: boolean): void {
|
|
26
|
+
const hasSetOps = !!(ast.setOps && ast.setOps.length);
|
|
27
|
+
if (!isOutermost && (ast.orderBy || ast.limit !== undefined || ast.offset !== undefined)) {
|
|
28
|
+
throw new Error('ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!hasSetOps) return;
|
|
32
|
+
|
|
33
|
+
for (const op of ast.setOps!) {
|
|
34
|
+
if (!this.supportsSetOperation(op.operator)) {
|
|
35
|
+
throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
|
|
36
|
+
}
|
|
37
|
+
this.validateSetOperations(op.query, false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private hoistCtes(ast: SelectQueryNode): {
|
|
42
|
+
normalized: SelectQueryNode;
|
|
43
|
+
hoistedCtes: CommonTableExpressionNode[];
|
|
44
|
+
} {
|
|
45
|
+
let hoisted: CommonTableExpressionNode[] = [];
|
|
46
|
+
|
|
47
|
+
const normalizedSetOps = ast.setOps?.map(op => {
|
|
48
|
+
const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
|
|
49
|
+
const childCtes = child.ctes ?? [];
|
|
50
|
+
if (childCtes.length) hoisted = hoisted.concat(childCtes);
|
|
51
|
+
hoisted = hoisted.concat(childHoisted);
|
|
52
|
+
const queryWithoutCtes = childCtes.length ? { ...child, ctes: undefined } : child;
|
|
53
|
+
return { ...op, query: queryWithoutCtes };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const normalized: SelectQueryNode = normalizedSetOps
|
|
57
|
+
? { ...ast, setOps: normalizedSetOps }
|
|
58
|
+
: ast;
|
|
59
|
+
return { normalized, hoistedCtes: hoisted };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DialectBase } from '../abstract.js';
|
|
2
|
+
import type { CompilerContext } from '../abstract.js';
|
|
3
|
+
import type {
|
|
3
4
|
SelectQueryNode,
|
|
4
5
|
InsertQueryNode,
|
|
5
6
|
UpdateQueryNode,
|
|
@@ -12,22 +13,22 @@ import {
|
|
|
12
13
|
OrderByNode,
|
|
13
14
|
TableNode
|
|
14
15
|
} from '../../ast/query.js';
|
|
15
|
-
import { ColumnNode, OperandNode } from '../../ast/expression.js';
|
|
16
|
+
import type { ColumnNode, OperandNode } from '../../ast/expression.js';
|
|
16
17
|
import { FunctionTableFormatter } from './function-table-formatter.js';
|
|
17
|
-
import {
|
|
18
|
+
import { StandardLimitOffsetPagination } from './pagination-strategy.js';
|
|
19
|
+
import type { PaginationStrategy } from './pagination-strategy.js';
|
|
18
20
|
import { CteCompiler } from './cte-compiler.js';
|
|
19
|
-
import {
|
|
21
|
+
import { NoReturningStrategy } from './returning-strategy.js';
|
|
22
|
+
import type { ReturningStrategy } from './returning-strategy.js';
|
|
20
23
|
import { JoinCompiler } from './join-compiler.js';
|
|
21
24
|
import { GroupByCompiler } from './groupby-compiler.js';
|
|
22
25
|
import { OrderByCompiler } from './orderby-compiler.js';
|
|
23
26
|
|
|
24
|
-
|
|
25
27
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Specific dialects should extend this class and implement dialect-specific logic.
|
|
28
|
+
* Reusable SQL implementation built on the structural Dialect contract.
|
|
29
|
+
* Dialects extend this only when its standard SELECT/DML behavior is useful.
|
|
29
30
|
*/
|
|
30
|
-
export abstract class SqlDialectBase extends
|
|
31
|
+
export abstract class SqlDialectBase extends DialectBase {
|
|
31
32
|
abstract quoteIdentifier(id: string): string;
|
|
32
33
|
|
|
33
34
|
protected paginationStrategy: PaginationStrategy = new StandardLimitOffsetPagination();
|
|
@@ -40,16 +41,14 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
40
41
|
ctx,
|
|
41
42
|
this.quoteIdentifier.bind(this),
|
|
42
43
|
this.compileSelectAst.bind(this),
|
|
43
|
-
this.normalizeSelectAst
|
|
44
|
+
this.normalizeSelectAst.bind(this),
|
|
44
45
|
this.stripTrailingSemicolon.bind(this)
|
|
45
46
|
);
|
|
46
47
|
const baseAst: SelectQueryNode = hasSetOps
|
|
47
48
|
? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
|
|
48
49
|
: ast;
|
|
49
50
|
const baseSelect = this.compileSelectCore(baseAst, ctx);
|
|
50
|
-
if (!hasSetOps) {
|
|
51
|
-
return `${ctes}${baseSelect}`;
|
|
52
|
-
}
|
|
51
|
+
if (!hasSetOps) return `${ctes}${baseSelect}`;
|
|
53
52
|
return this.compileSelectWithSetOps(ast, baseSelect, ctes, ctx);
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -116,9 +115,7 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
protected ensureConflictColumns(clause: UpsertClause, message: string): void {
|
|
119
|
-
if (!clause.target.columns.length)
|
|
120
|
-
throw new Error(message);
|
|
121
|
-
}
|
|
118
|
+
if (!clause.target.columns.length) throw new Error(message);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
@@ -159,8 +156,7 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
159
156
|
): string {
|
|
160
157
|
return assignments
|
|
161
158
|
.map(assignment => {
|
|
162
|
-
const
|
|
163
|
-
const target = this.compileSetTarget(col, table);
|
|
159
|
+
const target = this.compileSetTarget(assignment.column, table);
|
|
164
160
|
const value = this.compileOperand(assignment.value, ctx);
|
|
165
161
|
return `${target} = ${value}`;
|
|
166
162
|
})
|
|
@@ -175,13 +171,9 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
175
171
|
const baseTableName = table.name;
|
|
176
172
|
const alias = table.alias;
|
|
177
173
|
const columnTable = column.table ?? alias ?? baseTableName;
|
|
178
|
-
const tableQualifier =
|
|
179
|
-
alias && column.table === baseTableName ? alias : columnTable;
|
|
180
|
-
|
|
181
|
-
if (!tableQualifier) {
|
|
182
|
-
return this.quoteIdentifier(column.name);
|
|
183
|
-
}
|
|
174
|
+
const tableQualifier = alias && column.table === baseTableName ? alias : columnTable;
|
|
184
175
|
|
|
176
|
+
if (!tableQualifier) return this.quoteIdentifier(column.name);
|
|
185
177
|
return `${this.quoteIdentifier(tableQualifier)}.${this.quoteIdentifier(column.name)}`;
|
|
186
178
|
}
|
|
187
179
|
|
|
@@ -202,28 +194,21 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
202
194
|
}
|
|
203
195
|
|
|
204
196
|
protected compileSelectColumns(ast: SelectQueryNode, ctx: CompilerContext): string {
|
|
205
|
-
if (!ast.columns || ast.columns.length === 0)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (c.alias.includes('(')) return c.alias;
|
|
212
|
-
return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
|
|
197
|
+
if (!ast.columns || ast.columns.length === 0) return '*';
|
|
198
|
+
return ast.columns.map(column => {
|
|
199
|
+
const expr = this.compileOperand(column, ctx);
|
|
200
|
+
if (column.alias) {
|
|
201
|
+
if (column.alias.includes('(')) return column.alias;
|
|
202
|
+
return `${expr} AS ${this.quoteIdentifier(column.alias)}`;
|
|
213
203
|
}
|
|
214
204
|
return expr;
|
|
215
205
|
}).join(', ');
|
|
216
206
|
}
|
|
217
207
|
|
|
218
208
|
protected compileFrom(ast: SelectQueryNode['from'], ctx?: CompilerContext): string {
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
if (tableSource.type === 'DerivedTable') {
|
|
224
|
-
return this.compileDerivedTable(tableSource, ctx);
|
|
225
|
-
}
|
|
226
|
-
return this.compileTableSource(tableSource);
|
|
209
|
+
if (ast.type === 'FunctionTable') return this.compileFunctionTable(ast, ctx);
|
|
210
|
+
if (ast.type === 'DerivedTable') return this.compileDerivedTable(ast, ctx);
|
|
211
|
+
return this.compileTableSource(ast);
|
|
227
212
|
}
|
|
228
213
|
|
|
229
214
|
protected compileFunctionTable(fn: FunctionTableNode, ctx?: CompilerContext): string {
|
|
@@ -246,13 +231,14 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
246
231
|
}
|
|
247
232
|
}
|
|
248
233
|
|
|
249
|
-
return FunctionTableFormatter.format(fn, ctx,
|
|
234
|
+
return FunctionTableFormatter.format(fn, ctx, {
|
|
235
|
+
quoteIdentifier: id => this.quoteIdentifier(id),
|
|
236
|
+
compileOperand: (node, compilerContext) => this.compileOperand(node, compilerContext)
|
|
237
|
+
});
|
|
250
238
|
}
|
|
251
239
|
|
|
252
240
|
protected compileDerivedTable(table: DerivedTableNode, ctx?: CompilerContext): string {
|
|
253
|
-
if (!table.alias)
|
|
254
|
-
throw new Error('Derived tables must have an alias.');
|
|
255
|
-
}
|
|
241
|
+
if (!table.alias) throw new Error('Derived tables must have an alias.');
|
|
256
242
|
const subquery = this.compileSelectAst(this.normalizeSelectAst(table.query), ctx!).trim().replace(/;$/, '');
|
|
257
243
|
const columns = table.columnAliases?.length
|
|
258
244
|
? ` (${table.columnAliases.map(c => this.quoteIdentifier(c)).join(', ')})`
|
|
@@ -261,12 +247,8 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
261
247
|
}
|
|
262
248
|
|
|
263
249
|
protected compileTableSource(table: TableSourceNode): string {
|
|
264
|
-
if (table.type === 'FunctionTable')
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
if (table.type === 'DerivedTable') {
|
|
268
|
-
return this.compileDerivedTable(table as DerivedTableNode);
|
|
269
|
-
}
|
|
250
|
+
if (table.type === 'FunctionTable') return this.compileFunctionTable(table as FunctionTableNode);
|
|
251
|
+
if (table.type === 'DerivedTable') return this.compileDerivedTable(table as DerivedTableNode);
|
|
270
252
|
const base = this.compileTableName(table);
|
|
271
253
|
return table.alias ? `${base} AS ${this.quoteIdentifier(table.alias)}` : base;
|
|
272
254
|
}
|
|
@@ -285,9 +267,7 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
285
267
|
|
|
286
268
|
private compileUpdateFromClause(ast: UpdateQueryNode, ctx: CompilerContext): string {
|
|
287
269
|
if (!ast.from && (!ast.joins || ast.joins.length === 0)) return '';
|
|
288
|
-
if (!ast.from)
|
|
289
|
-
throw new Error('UPDATE with JOINs requires an explicit FROM clause.');
|
|
290
|
-
}
|
|
270
|
+
if (!ast.from) throw new Error('UPDATE with JOINs requires an explicit FROM clause.');
|
|
291
271
|
const from = this.compileFrom(ast.from, ctx);
|
|
292
272
|
const joins = JoinCompiler.compileJoins(
|
|
293
273
|
ast.joins,
|
|
@@ -300,9 +280,7 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
300
280
|
|
|
301
281
|
private compileDeleteUsingClause(ast: DeleteQueryNode, ctx: CompilerContext): string {
|
|
302
282
|
if (!ast.using && (!ast.joins || ast.joins.length === 0)) return '';
|
|
303
|
-
if (!ast.using)
|
|
304
|
-
throw new Error('DELETE with JOINs requires a USING clause.');
|
|
305
|
-
}
|
|
283
|
+
if (!ast.using) throw new Error('DELETE with JOINs requires a USING clause.');
|
|
306
284
|
const usingTable = this.compileFrom(ast.using, ctx);
|
|
307
285
|
const joins = JoinCompiler.compileJoins(
|
|
308
286
|
ast.joins,
|
|
@@ -323,8 +301,7 @@ export abstract class SqlDialectBase extends Dialect {
|
|
|
323
301
|
}
|
|
324
302
|
|
|
325
303
|
protected wrapSetOperand(sql: string): string {
|
|
326
|
-
|
|
327
|
-
return `(${trimmed})`;
|
|
304
|
+
return `(${this.stripTrailingSemicolon(sql)})`;
|
|
328
305
|
}
|
|
329
306
|
|
|
330
307
|
protected renderOrderByNulls(order: OrderByNode): string | undefined {
|