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.
@@ -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
- import { SqlDialectBase } from './sql-dialect.js';
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
- * Formatter for function table expressions (e.g., LATERAL unnest(...) WITH ORDINALITY).
8
- * Encapsulates logic for generating SQL function table syntax including LATERAL, aliases, and column lists.
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
- * Formats a function table node into SQL syntax.
13
- * @param fn - The function table node containing schema, name, args, and aliases.
14
- * @param ctx - Optional compiler context for operand compilation.
15
- * @param dialect - The dialect instance for compiling operands.
16
- * @returns SQL function table expression (e.g., "LATERAL schema.func(args) WITH ORDINALITY AS alias(col1, col2)").
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, dialect);
24
- const colAliases = this.formatColumnAliases(fn, dialect);
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
- * Formats the schema prefix for the function name.
30
- * @param fn - The function table node.
31
- * @param dialect - The dialect instance for quoting identifiers.
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
- const quoted = dialect ? dialect.quoteIdentifier(fn.schema) : fn.schema;
38
- return `${quoted}.`;
34
+ return `${formatter.quoteIdentifier(fn.schema)}.`;
39
35
  }
40
36
 
41
- /**
42
- * Formats function arguments into SQL syntax.
43
- * @param fn - The function table node containing arguments.
44
- * @param ctx - Optional compiler context for operand compilation.
45
- * @param dialect - The dialect instance for compiling operands.
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((a: OperandNode) => {
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
- * Formats the table alias for the function table.
86
- * @param fn - The function table node.
87
- * @param dialect - The dialect instance for quoting identifiers.
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
- const quoted = dialect ? dialect.quoteIdentifier(fn.alias) : fn.alias;
94
- return ` AS ${quoted}`;
61
+ return ` AS ${formatter.quoteIdentifier(fn.alias)}`;
95
62
  }
96
63
 
97
- /**
98
- * Formats column aliases for the function table result columns.
99
- * @param fn - The function table node containing column aliases.
100
- * @param dialect - The dialect instance for quoting identifiers.
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 => dialect ? dialect.quoteIdentifier(col) : 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
+ }