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