metal-orm 1.1.22 → 1.1.24
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 +564 -575
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -164
- package/dist/index.d.ts +196 -164
- package/dist/index.js +554 -575
- 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 +84 -245
- package/src/core/dialect/base/standard-delete-compiler.ts +37 -0
- package/src/core/dialect/base/standard-insert-compiler.ts +49 -0
- package/src/core/dialect/base/standard-select-compiler.ts +82 -0
- package/src/core/dialect/base/standard-sql-services.ts +44 -0
- package/src/core/dialect/base/standard-sql-source-compiler.ts +88 -0
- package/src/core/dialect/base/standard-update-compiler.ts +53 -0
- 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 +10 -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
|
+
}
|