@mikro-orm/sql 7.0.17 → 7.0.18-dev.0
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/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +27 -6
- package/AbstractSqlDriver.d.ts +25 -1
- package/AbstractSqlDriver.js +356 -20
- package/AbstractSqlPlatform.d.ts +13 -2
- package/AbstractSqlPlatform.js +16 -3
- package/PivotCollectionPersister.d.ts +2 -2
- package/PivotCollectionPersister.js +19 -3
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +46 -3
- package/SqlEntityManager.js +77 -7
- package/SqlMikroORM.d.ts +4 -4
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +19 -3
- package/dialects/mysql/MySqlSchemaHelper.js +254 -21
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
- package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +8 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +50 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +38 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +341 -6
- package/dialects/postgresql/index.d.ts +2 -0
- package/dialects/postgresql/index.js +2 -0
- package/dialects/postgresql/typeOverrides.d.ts +14 -0
- package/dialects/postgresql/typeOverrides.js +12 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +7 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +131 -2
- package/package.json +2 -2
- package/query/NativeQueryBuilder.d.ts +6 -0
- package/query/NativeQueryBuilder.js +16 -1
- package/query/QueryBuilder.d.ts +83 -1
- package/query/QueryBuilder.js +181 -8
- package/schema/DatabaseSchema.d.ts +29 -2
- package/schema/DatabaseSchema.js +137 -0
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +62 -3
- package/schema/SchemaComparator.d.ts +19 -0
- package/schema/SchemaComparator.js +250 -1
- package/schema/SchemaHelper.d.ts +77 -1
- package/schema/SchemaHelper.js +279 -5
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +47 -10
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +69 -2
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type Connection, type Dictionary, type Options, type Transaction, type RawQueryFragment } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
|
|
3
3
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
4
|
-
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../typings.js';
|
|
4
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef, SqlRoutineDef } from '../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from './DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from './DatabaseTable.js';
|
|
7
|
+
/** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
|
|
8
|
+
export declare function stripStatementNewlines(body: string): string;
|
|
7
9
|
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
|
|
8
10
|
export declare abstract class SchemaHelper {
|
|
9
11
|
protected readonly platform: AbstractSqlPlatform;
|
|
@@ -16,6 +18,14 @@ export declare abstract class SchemaHelper {
|
|
|
16
18
|
enableForeignKeysSQL(): string;
|
|
17
19
|
/** Returns SQL to append to schema migration scripts (e.g., re-enabling FK checks). */
|
|
18
20
|
getSchemaEnd(disableForeignKeys?: boolean): string;
|
|
21
|
+
/** Sets the current schema for the session (e.g. `SET search_path`). */
|
|
22
|
+
getSetSchemaSQL(_schema: string): string;
|
|
23
|
+
/** Whether the driver supports setting a runtime schema per migration run. */
|
|
24
|
+
supportsMigrationSchema(): boolean;
|
|
25
|
+
/** Restores the session's schema to the connection's default after a migration. */
|
|
26
|
+
getResetSchemaSQL(_defaultSchema: string): string;
|
|
27
|
+
/** Returns `undefined` for schemaless drivers, throws for drivers that have schemas but no session switch. */
|
|
28
|
+
resolveMigrationSchema(schema: string | undefined): string | undefined;
|
|
19
29
|
finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
|
|
20
30
|
appendComments(table: DatabaseTable): string[];
|
|
21
31
|
supportsSchemaConstraints(): boolean;
|
|
@@ -31,6 +41,8 @@ export declare abstract class SchemaHelper {
|
|
|
31
41
|
getListTablesSQL(): string;
|
|
32
42
|
/** Retrieves all tables from the database. */
|
|
33
43
|
getAllTables(connection: AbstractSqlConnection, schemas?: string[], ctx?: Transaction): Promise<Table[]>;
|
|
44
|
+
/** Checks whether a specific table exists in a given schema (not the connection's current schema). */
|
|
45
|
+
tableExists(connection: AbstractSqlConnection, tableName: string, schemaName: string | undefined, ctx?: Transaction): Promise<boolean>;
|
|
34
46
|
getListViewsSQL(): string;
|
|
35
47
|
loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string, ctx?: Transaction): Promise<void>;
|
|
36
48
|
/** Returns SQL to rename a column in a table. */
|
|
@@ -41,6 +53,46 @@ export declare abstract class SchemaHelper {
|
|
|
41
53
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
42
54
|
*/
|
|
43
55
|
protected getCreateIndexSuffix(_index: IndexDef): string;
|
|
56
|
+
/**
|
|
57
|
+
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
58
|
+
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
59
|
+
* with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
|
|
60
|
+
* entirely via an override on `getIndexColumns`.
|
|
61
|
+
*/
|
|
62
|
+
protected getIndexWhereClause(index: IndexDef): string;
|
|
63
|
+
/**
|
|
64
|
+
* Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
|
|
65
|
+
* emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
|
|
66
|
+
* being treated as distinct in unique indexes, this enforces uniqueness only where the
|
|
67
|
+
* predicate holds. Throws if combined with the advanced `columns` option.
|
|
68
|
+
*/
|
|
69
|
+
protected emulatePartialIndexColumns(index: IndexDef): string;
|
|
70
|
+
/**
|
|
71
|
+
* Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
|
|
72
|
+
* introspected partial-index predicate when the column matches one of the index's own
|
|
73
|
+
* columns. MikroORM auto-emits this guard for unique indexes on nullable columns
|
|
74
|
+
* (MSSQL, Oracle) — it's an internal artifact, not user intent.
|
|
75
|
+
*
|
|
76
|
+
* Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
|
|
77
|
+
* appends a single guard per index column. This preserves user intent when they redundantly
|
|
78
|
+
* include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
|
|
79
|
+
* their copy survives.
|
|
80
|
+
*/
|
|
81
|
+
protected stripAutoNotNullFilter(filterDef: string, columnNames: string[], identifierPattern: RegExp): string;
|
|
82
|
+
/**
|
|
83
|
+
* Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
|
|
84
|
+
* `[` for array literals/constructors or never produce it in introspected predicates,
|
|
85
|
+
* so the default is `false` and the MSSQL helper opts in.
|
|
86
|
+
*/
|
|
87
|
+
protected get bracketQuotedIdentifiers(): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
|
|
90
|
+
* literals, quoted identifiers, or parenthesized groups — so a predicate like
|
|
91
|
+
* `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
|
|
92
|
+
*/
|
|
93
|
+
protected splitTopLevelAnd(s: string): string[];
|
|
94
|
+
/** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
|
|
95
|
+
protected isBalancedWrap(s: string): boolean;
|
|
44
96
|
/**
|
|
45
97
|
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
46
98
|
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
@@ -57,6 +109,8 @@ export declare abstract class SchemaHelper {
|
|
|
57
109
|
hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
|
|
58
110
|
castColumn(name: string, type: string): string;
|
|
59
111
|
alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
|
|
112
|
+
/** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
|
|
113
|
+
protected getCollateSQL(collation: string): string;
|
|
60
114
|
createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
|
|
61
115
|
getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
62
116
|
getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
@@ -84,6 +138,28 @@ export declare abstract class SchemaHelper {
|
|
|
84
138
|
getReferencedTableName(referencedTableName: string, schema?: string): string;
|
|
85
139
|
createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
|
|
86
140
|
createCheck(table: DatabaseTable, check: CheckDef): string;
|
|
141
|
+
/**
|
|
142
|
+
* Generates SQL to create a database trigger on a table.
|
|
143
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
144
|
+
*/
|
|
145
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
146
|
+
/**
|
|
147
|
+
* Generates SQL to drop a database trigger from a table.
|
|
148
|
+
* Override in driver-specific helpers for custom DDL.
|
|
149
|
+
*/
|
|
150
|
+
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
151
|
+
/** Default no-op so SQLite/libSQL silent-skip routine DDL; routine-capable dialects override. */
|
|
152
|
+
createRoutine(_routine: SqlRoutineDef): string;
|
|
153
|
+
dropRoutine(_routine: SqlRoutineDef): string;
|
|
154
|
+
getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
155
|
+
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
|
|
156
|
+
protected wrapRoutineBody(body: string): string;
|
|
157
|
+
protected stripRoutineBody(body: string): string;
|
|
158
|
+
/** T-SQL requires `@name` inside the body; PG/MySQL/Oracle use the bare name. */
|
|
159
|
+
routineParamReference(name: string): string;
|
|
160
|
+
/** T-SQL doesn't distinguish `OUT` from `INOUT` in the catalog — overrides fold `'out'` into `'inout'`. */
|
|
161
|
+
normaliseRoutineParamDirection(direction: 'in' | 'out' | 'inout'): 'in' | 'out' | 'inout';
|
|
162
|
+
protected qualifiedRoutineName(routine: SqlRoutineDef): string;
|
|
87
163
|
/** @internal */
|
|
88
164
|
getTableName(table: string, schema?: string): string;
|
|
89
165
|
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { isRaw, Utils, } from '@mikro-orm/core';
|
|
2
|
+
/** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
|
|
3
|
+
export function stripStatementNewlines(body) {
|
|
4
|
+
return body.replace(/;[\t ]*\r?\n/g, '; ');
|
|
5
|
+
}
|
|
2
6
|
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
|
|
3
7
|
export class SchemaHelper {
|
|
4
8
|
platform;
|
|
@@ -27,6 +31,32 @@ export class SchemaHelper {
|
|
|
27
31
|
}
|
|
28
32
|
return '';
|
|
29
33
|
}
|
|
34
|
+
/** Sets the current schema for the session (e.g. `SET search_path`). */
|
|
35
|
+
getSetSchemaSQL(_schema) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
/** Whether the driver supports setting a runtime schema per migration run. */
|
|
39
|
+
supportsMigrationSchema() {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/** Restores the session's schema to the connection's default after a migration. */
|
|
43
|
+
getResetSchemaSQL(_defaultSchema) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
/** Returns `undefined` for schemaless drivers, throws for drivers that have schemas but no session switch. */
|
|
47
|
+
resolveMigrationSchema(schema) {
|
|
48
|
+
if (!schema) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (this.supportsMigrationSchema()) {
|
|
52
|
+
return schema;
|
|
53
|
+
}
|
|
54
|
+
if (!this.platform.supportsSchemas()) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const driverName = this.platform.constructor.name.replace(/Platform$/, 'Driver');
|
|
58
|
+
throw new Error(`Runtime schema for migrations is not supported by the ${driverName}`);
|
|
59
|
+
}
|
|
30
60
|
finalizeTable(table, charset, collate) {
|
|
31
61
|
return '';
|
|
32
62
|
}
|
|
@@ -75,6 +105,13 @@ export class SchemaHelper {
|
|
|
75
105
|
async getAllTables(connection, schemas, ctx) {
|
|
76
106
|
return connection.execute(this.getListTablesSQL(), [], 'all', ctx);
|
|
77
107
|
}
|
|
108
|
+
/** Checks whether a specific table exists in a given schema (not the connection's current schema). */
|
|
109
|
+
async tableExists(connection, tableName, schemaName, ctx) {
|
|
110
|
+
const qv = (v) => this.platform.quoteValue(v ?? '');
|
|
111
|
+
const resolved = schemaName ?? this.platform.getDefaultSchemaName();
|
|
112
|
+
const rows = await connection.execute(`select 1 from information_schema.tables where table_schema = ${qv(resolved)} and table_name = ${qv(tableName)}`, [], 'all', ctx);
|
|
113
|
+
return rows.length > 0;
|
|
114
|
+
}
|
|
78
115
|
getListViewsSQL() {
|
|
79
116
|
throw new Error('Not supported by given driver');
|
|
80
117
|
}
|
|
@@ -110,7 +147,7 @@ export class SchemaHelper {
|
|
|
110
147
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
111
148
|
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
|
|
112
149
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
113
|
-
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${defer}`;
|
|
150
|
+
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${this.getIndexWhereClause(index)}${defer}`;
|
|
114
151
|
}
|
|
115
152
|
// Build column list with advanced options
|
|
116
153
|
const columns = this.getIndexColumns(index);
|
|
@@ -119,7 +156,7 @@ export class SchemaHelper {
|
|
|
119
156
|
if (index.include?.length) {
|
|
120
157
|
sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
|
|
121
158
|
}
|
|
122
|
-
return sql + this.getCreateIndexSuffix(index) + defer;
|
|
159
|
+
return sql + this.getCreateIndexSuffix(index) + this.getIndexWhereClause(index) + defer;
|
|
123
160
|
}
|
|
124
161
|
/**
|
|
125
162
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
@@ -127,6 +164,153 @@ export class SchemaHelper {
|
|
|
127
164
|
getCreateIndexSuffix(_index) {
|
|
128
165
|
return '';
|
|
129
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
169
|
+
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
170
|
+
* with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
|
|
171
|
+
* entirely via an override on `getIndexColumns`.
|
|
172
|
+
*/
|
|
173
|
+
getIndexWhereClause(index) {
|
|
174
|
+
return index.where ? ` where ${index.where}` : '';
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
|
|
178
|
+
* emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
|
|
179
|
+
* being treated as distinct in unique indexes, this enforces uniqueness only where the
|
|
180
|
+
* predicate holds. Throws if combined with the advanced `columns` option.
|
|
181
|
+
*/
|
|
182
|
+
emulatePartialIndexColumns(index) {
|
|
183
|
+
if (index.columns?.length) {
|
|
184
|
+
throw new Error(`Index '${index.keyName}': combining \`where\` with advanced \`columns\` options is not supported when emulating a partial index via functional expressions; use plain \`properties\` (or \`columnNames\`).`);
|
|
185
|
+
}
|
|
186
|
+
const predicate = index.where;
|
|
187
|
+
return index.columnNames.map(c => `(case when ${predicate} then ${this.quote(c)} end)`).join(', ');
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
|
|
191
|
+
* introspected partial-index predicate when the column matches one of the index's own
|
|
192
|
+
* columns. MikroORM auto-emits this guard for unique indexes on nullable columns
|
|
193
|
+
* (MSSQL, Oracle) — it's an internal artifact, not user intent.
|
|
194
|
+
*
|
|
195
|
+
* Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
|
|
196
|
+
* appends a single guard per index column. This preserves user intent when they redundantly
|
|
197
|
+
* include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
|
|
198
|
+
* their copy survives.
|
|
199
|
+
*/
|
|
200
|
+
stripAutoNotNullFilter(filterDef, columnNames, identifierPattern) {
|
|
201
|
+
// Peel off any number of balanced wrapping paren layers. Introspection sources differ
|
|
202
|
+
// (MSSQL `filter_definition` wraps once, Oracle `INDEX_EXPRESSIONS` typically not at all),
|
|
203
|
+
// and a user `where` round-tripped through a dialect that double-wraps would otherwise slip
|
|
204
|
+
// past the auto-NOT-NULL recognizer below.
|
|
205
|
+
let inner = filterDef.trim();
|
|
206
|
+
while (inner.startsWith('(') && inner.endsWith(')') && this.isBalancedWrap(inner)) {
|
|
207
|
+
inner = inner.slice(1, -1).trim();
|
|
208
|
+
}
|
|
209
|
+
const clauses = this.splitTopLevelAnd(inner);
|
|
210
|
+
const autoCol = (clause) => {
|
|
211
|
+
let trimmed = clause.trim();
|
|
212
|
+
while (trimmed.startsWith('(') && trimmed.endsWith(')') && this.isBalancedWrap(trimmed)) {
|
|
213
|
+
trimmed = trimmed.slice(1, -1).trim();
|
|
214
|
+
}
|
|
215
|
+
const match = identifierPattern.exec(trimmed);
|
|
216
|
+
return match && columnNames.includes(match[1]) ? match[1] : null;
|
|
217
|
+
};
|
|
218
|
+
const seen = new Set();
|
|
219
|
+
const kept = [];
|
|
220
|
+
for (let i = clauses.length - 1; i >= 0; i--) {
|
|
221
|
+
const col = autoCol(clauses[i]);
|
|
222
|
+
if (col && !seen.has(col)) {
|
|
223
|
+
seen.add(col);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
kept.unshift(clauses[i]);
|
|
227
|
+
}
|
|
228
|
+
return kept.join(' and ').trim();
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
|
|
232
|
+
* `[` for array literals/constructors or never produce it in introspected predicates,
|
|
233
|
+
* so the default is `false` and the MSSQL helper opts in.
|
|
234
|
+
*/
|
|
235
|
+
get bracketQuotedIdentifiers() {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
|
|
240
|
+
* literals, quoted identifiers, or parenthesized groups — so a predicate like
|
|
241
|
+
* `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
|
|
242
|
+
*/
|
|
243
|
+
splitTopLevelAnd(s) {
|
|
244
|
+
const parts = [];
|
|
245
|
+
let depth = 0;
|
|
246
|
+
let quote = null;
|
|
247
|
+
let start = 0;
|
|
248
|
+
let i = 0;
|
|
249
|
+
while (i < s.length) {
|
|
250
|
+
const c = s[i];
|
|
251
|
+
if (quote) {
|
|
252
|
+
// Handle SQL's doubled-delimiter escape inside quoted strings/identifiers:
|
|
253
|
+
// `'` → `''`, `"` → `""`, `` ` `` → ```` `` ````, MSSQL `]` → `]]`.
|
|
254
|
+
if (c === quote && s[i + 1] === quote) {
|
|
255
|
+
i += 2;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (c === quote) {
|
|
259
|
+
quote = null;
|
|
260
|
+
}
|
|
261
|
+
i++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
265
|
+
quote = c;
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (c === '[' && this.bracketQuotedIdentifiers) {
|
|
270
|
+
quote = ']';
|
|
271
|
+
i++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (c === '(') {
|
|
275
|
+
depth++;
|
|
276
|
+
i++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (c === ')') {
|
|
280
|
+
depth--;
|
|
281
|
+
i++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (depth === 0 && /\s/.test(c)) {
|
|
285
|
+
const m = /^\s+and\s+/i.exec(s.slice(i));
|
|
286
|
+
if (m) {
|
|
287
|
+
parts.push(s.slice(start, i).trim());
|
|
288
|
+
i += m[0].length;
|
|
289
|
+
start = i;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
i++;
|
|
294
|
+
}
|
|
295
|
+
parts.push(s.slice(start).trim());
|
|
296
|
+
return parts.filter(p => p.length > 0);
|
|
297
|
+
}
|
|
298
|
+
/** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
|
|
299
|
+
isBalancedWrap(s) {
|
|
300
|
+
let depth = 0;
|
|
301
|
+
for (let i = 0; i < s.length; i++) {
|
|
302
|
+
if (s[i] === '(') {
|
|
303
|
+
depth++;
|
|
304
|
+
}
|
|
305
|
+
else if (s[i] === ')') {
|
|
306
|
+
depth--;
|
|
307
|
+
if (depth === 0 && i < s.length - 1) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return depth === 0;
|
|
313
|
+
}
|
|
130
314
|
/**
|
|
131
315
|
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
132
316
|
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
@@ -202,6 +386,12 @@ export class SchemaHelper {
|
|
|
202
386
|
for (const check of Object.values(diff.changedChecks)) {
|
|
203
387
|
ret.push(this.dropConstraint(diff.name, check.name));
|
|
204
388
|
}
|
|
389
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
390
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
391
|
+
}
|
|
392
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
393
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
394
|
+
}
|
|
205
395
|
/* v8 ignore next */
|
|
206
396
|
if (!safe && Object.values(diff.removedColumns).length > 0) {
|
|
207
397
|
ret.push(this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName));
|
|
@@ -226,7 +416,7 @@ export class SchemaHelper {
|
|
|
226
416
|
this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
|
|
227
417
|
}
|
|
228
418
|
for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
|
|
229
|
-
if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems'].some(t => changedProperties.has(t))) {
|
|
419
|
+
if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
|
|
230
420
|
continue; // will be handled via column update
|
|
231
421
|
}
|
|
232
422
|
ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
|
|
@@ -261,6 +451,12 @@ export class SchemaHelper {
|
|
|
261
451
|
for (const check of Object.values(diff.changedChecks)) {
|
|
262
452
|
ret.push(this.createCheck(diff.toTable, check));
|
|
263
453
|
}
|
|
454
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
455
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
456
|
+
}
|
|
457
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
458
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
459
|
+
}
|
|
264
460
|
if ('changedComment' in diff) {
|
|
265
461
|
ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
|
|
266
462
|
}
|
|
@@ -297,7 +493,7 @@ export class SchemaHelper {
|
|
|
297
493
|
if (changedProperties.has('default') && column.default == null) {
|
|
298
494
|
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} drop default`);
|
|
299
495
|
}
|
|
300
|
-
if (changedProperties.has('type')) {
|
|
496
|
+
if (changedProperties.has('type') || changedProperties.has('collation')) {
|
|
301
497
|
let type = column.type + (column.generated ? ` generated always as ${column.generated}` : '');
|
|
302
498
|
if (column.nativeEnumName) {
|
|
303
499
|
const parts = type.split('.');
|
|
@@ -309,7 +505,8 @@ export class SchemaHelper {
|
|
|
309
505
|
}
|
|
310
506
|
type = this.quote(type);
|
|
311
507
|
}
|
|
312
|
-
|
|
508
|
+
const collateClause = column.collation ? ` ${this.getCollateSQL(column.collation)}` : '';
|
|
509
|
+
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + collateClause + this.castColumn(column.name, type)}`);
|
|
313
510
|
}
|
|
314
511
|
if (changedProperties.has('default') && column.default != null) {
|
|
315
512
|
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} set default ${column.default}`);
|
|
@@ -320,6 +517,11 @@ export class SchemaHelper {
|
|
|
320
517
|
}
|
|
321
518
|
return sql;
|
|
322
519
|
}
|
|
520
|
+
/** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
|
|
521
|
+
getCollateSQL(collation) {
|
|
522
|
+
this.platform.validateCollationName(collation);
|
|
523
|
+
return `collate ${collation}`;
|
|
524
|
+
}
|
|
323
525
|
createTableColumn(column, table, changedProperties) {
|
|
324
526
|
const compositePK = table.getPrimaryKey()?.composite;
|
|
325
527
|
const primaryKey = !changedProperties && !this.hasNonDefaultPrimaryKeyName(table);
|
|
@@ -327,6 +529,7 @@ export class SchemaHelper {
|
|
|
327
529
|
const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement;
|
|
328
530
|
const col = [this.quote(column.name), columnType];
|
|
329
531
|
Utils.runIfNotEmpty(() => col.push('unsigned'), column.unsigned && this.platform.supportsUnsigned());
|
|
532
|
+
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
330
533
|
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
|
|
331
534
|
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
|
|
332
535
|
Utils.runIfNotEmpty(() => col.push('auto_increment'), column.autoincrement);
|
|
@@ -519,6 +722,9 @@ export class SchemaHelper {
|
|
|
519
722
|
for (const check of table.getChecks()) {
|
|
520
723
|
this.append(ret, this.createCheck(table, check));
|
|
521
724
|
}
|
|
725
|
+
for (const trigger of table.getTriggers()) {
|
|
726
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
727
|
+
}
|
|
522
728
|
}
|
|
523
729
|
return ret;
|
|
524
730
|
}
|
|
@@ -598,6 +804,74 @@ export class SchemaHelper {
|
|
|
598
804
|
createCheck(table, check) {
|
|
599
805
|
return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
|
|
600
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* Generates SQL to create a database trigger on a table.
|
|
809
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
810
|
+
*/
|
|
811
|
+
/* v8 ignore next 10 */
|
|
812
|
+
createTrigger(table, trigger) {
|
|
813
|
+
if (trigger.expression) {
|
|
814
|
+
return trigger.expression;
|
|
815
|
+
}
|
|
816
|
+
const timing = trigger.timing.toUpperCase();
|
|
817
|
+
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
818
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
819
|
+
const when = trigger.when ? ` when (${trigger.when})` : '';
|
|
820
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Generates SQL to drop a database trigger from a table.
|
|
824
|
+
* Override in driver-specific helpers for custom DDL.
|
|
825
|
+
*/
|
|
826
|
+
dropTrigger(table, trigger) {
|
|
827
|
+
if (trigger.events.length > 1) {
|
|
828
|
+
return trigger.events
|
|
829
|
+
.map(event => `drop trigger if exists ${this.quote(`${trigger.name}_${event}`)}`)
|
|
830
|
+
.join(';\n');
|
|
831
|
+
}
|
|
832
|
+
return `drop trigger if exists ${this.quote(trigger.name)}`;
|
|
833
|
+
}
|
|
834
|
+
/** Default no-op so SQLite/libSQL silent-skip routine DDL; routine-capable dialects override. */
|
|
835
|
+
createRoutine(_routine) {
|
|
836
|
+
return '';
|
|
837
|
+
}
|
|
838
|
+
dropRoutine(_routine) {
|
|
839
|
+
return '';
|
|
840
|
+
}
|
|
841
|
+
async getAllRoutines(_connection, _schemas = []) {
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
|
|
845
|
+
wrapRoutineBody(body) {
|
|
846
|
+
const trimmed = stripStatementNewlines(body).trim();
|
|
847
|
+
if (/^begin\b/i.test(trimmed)) {
|
|
848
|
+
return trimmed;
|
|
849
|
+
}
|
|
850
|
+
const withSemi = /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
|
|
851
|
+
return `begin ${withSemi} end`;
|
|
852
|
+
}
|
|
853
|
+
stripRoutineBody(body) {
|
|
854
|
+
const match = /^\s*begin\s+([\s\S]*?)\s*end\s*;?\s*$/i.exec(body.trim());
|
|
855
|
+
if (match) {
|
|
856
|
+
return match[1].trim().replace(/;\s*$/, '');
|
|
857
|
+
}
|
|
858
|
+
return body.trim();
|
|
859
|
+
}
|
|
860
|
+
/** T-SQL requires `@name` inside the body; PG/MySQL/Oracle use the bare name. */
|
|
861
|
+
routineParamReference(name) {
|
|
862
|
+
return name;
|
|
863
|
+
}
|
|
864
|
+
/** T-SQL doesn't distinguish `OUT` from `INOUT` in the catalog — overrides fold `'out'` into `'inout'`. */
|
|
865
|
+
normaliseRoutineParamDirection(direction) {
|
|
866
|
+
return direction;
|
|
867
|
+
}
|
|
868
|
+
qualifiedRoutineName(routine) {
|
|
869
|
+
const defaultSchema = this.platform.getDefaultSchemaName();
|
|
870
|
+
if (routine.schema && routine.schema !== defaultSchema) {
|
|
871
|
+
return `${this.platform.quoteIdentifier(routine.schema)}.${this.platform.quoteIdentifier(routine.name)}`;
|
|
872
|
+
}
|
|
873
|
+
return this.platform.quoteIdentifier(routine.name);
|
|
874
|
+
}
|
|
601
875
|
/** @internal */
|
|
602
876
|
getTableName(table, schema) {
|
|
603
877
|
if (schema && schema !== this.platform.getDefaultSchemaName()) {
|
|
@@ -15,8 +15,8 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
|
|
|
15
15
|
* Returns true if the database was created.
|
|
16
16
|
*/
|
|
17
17
|
ensureDatabase(options?: EnsureDatabaseOptions): Promise<boolean>;
|
|
18
|
-
getTargetSchema(schema?: string): DatabaseSchema;
|
|
19
|
-
protected getOrderedMetadata(schema?: string): EntityMetadata[];
|
|
18
|
+
getTargetSchema(schema?: string, includeWildcardSchema?: boolean): DatabaseSchema;
|
|
19
|
+
protected getOrderedMetadata(schema?: string, includeWildcardSchema?: boolean): EntityMetadata[];
|
|
20
20
|
getCreateSchemaSQL(options?: CreateSchemaOptions): Promise<string>;
|
|
21
21
|
drop(options?: DropSchemaOptions): Promise<void>;
|
|
22
22
|
createNamespace(name: string): Promise<void>;
|
|
@@ -44,13 +44,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
44
44
|
}
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
|
-
getTargetSchema(schema) {
|
|
48
|
-
const metadata = this.getOrderedMetadata(schema);
|
|
47
|
+
getTargetSchema(schema, includeWildcardSchema = false) {
|
|
48
|
+
const metadata = this.getOrderedMetadata(schema, includeWildcardSchema);
|
|
49
49
|
const schemaName = schema ?? this.config.get('schema') ?? this.platform.getDefaultSchemaName();
|
|
50
|
-
|
|
50
|
+
const target = DatabaseSchema.fromMetadata(metadata, this.platform, this.config, schemaName, this.em);
|
|
51
|
+
const routines = this.config.getRoutines();
|
|
52
|
+
if (routines.length > 0) {
|
|
53
|
+
target.addRoutinesFromMetadata(routines, this.platform, this.em);
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
51
56
|
}
|
|
52
|
-
getOrderedMetadata(schema) {
|
|
53
|
-
const metadata = super.getOrderedMetadata(schema);
|
|
57
|
+
getOrderedMetadata(schema, includeWildcardSchema = false) {
|
|
58
|
+
const metadata = super.getOrderedMetadata(schema, includeWildcardSchema);
|
|
54
59
|
// Filter out skipped tables
|
|
55
60
|
return metadata.filter(meta => {
|
|
56
61
|
const tableName = meta.tableName;
|
|
@@ -59,7 +64,7 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
59
64
|
});
|
|
60
65
|
}
|
|
61
66
|
async getCreateSchemaSQL(options = {}) {
|
|
62
|
-
const toSchema = this.getTargetSchema(options.schema);
|
|
67
|
+
const toSchema = this.getTargetSchema(options.schema, options.includeWildcardSchema);
|
|
63
68
|
const ret = [];
|
|
64
69
|
for (const namespace of toSchema.getNamespaces()) {
|
|
65
70
|
if (namespace === this.platform.getDefaultSchemaName()) {
|
|
@@ -93,6 +98,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
93
98
|
for (const view of sortedViews) {
|
|
94
99
|
this.appendViewCreation(ret, view);
|
|
95
100
|
}
|
|
101
|
+
for (const routine of toSchema.getRoutines()) {
|
|
102
|
+
// pad=true so each routine is its own batch — MSSQL requires CREATE PROC to be first in a batch.
|
|
103
|
+
this.append(ret, this.helper.createRoutine(routine), true);
|
|
104
|
+
}
|
|
96
105
|
return this.wrapSchema(ret, options);
|
|
97
106
|
}
|
|
98
107
|
async drop(options = {}) {
|
|
@@ -161,6 +170,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
161
170
|
this.append(ret, this.helper.dropViewIfExists(view.name, view.schema));
|
|
162
171
|
}
|
|
163
172
|
}
|
|
173
|
+
// Drop routines before tables - most stored routines reference table columns.
|
|
174
|
+
for (const routine of targetSchema.getRoutines()) {
|
|
175
|
+
this.append(ret, this.helper.dropRoutine(routine), true);
|
|
176
|
+
}
|
|
164
177
|
// remove FKs explicitly if we can't use a cascading statement and we don't disable FK checks (we need this for circular relations)
|
|
165
178
|
for (const meta of metadata) {
|
|
166
179
|
if (!this.platform.usesCascadeStatement() && (!this.options.disableForeignKeys || options.dropForeignKeys)) {
|
|
@@ -225,13 +238,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
225
238
|
async prepareSchemaForComparison(options) {
|
|
226
239
|
options.safe ??= false;
|
|
227
240
|
options.dropTables ??= true;
|
|
228
|
-
const toSchema = this.getTargetSchema(options.schema);
|
|
241
|
+
const toSchema = this.getTargetSchema(options.schema, options.includeWildcardSchema);
|
|
229
242
|
const schemas = toSchema.getNamespaces();
|
|
230
243
|
const fromSchema = options.fromSchema ??
|
|
231
244
|
(await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables, this.options.skipViews));
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
// Always load DB routines so orphans are detected when the user removes the last metadata
|
|
246
|
+
// routine. Dialects without routine support return []; that's the silent-skip path.
|
|
247
|
+
if (options.fromSchema == null) {
|
|
248
|
+
await fromSchema.loadRoutines(this.connection, this.platform, [...schemas]);
|
|
249
|
+
}
|
|
250
|
+
const wildcardSchemaTables = options.includeWildcardSchema
|
|
251
|
+
? []
|
|
252
|
+
: [...this.metadata.getAll().values()].filter(meta => meta.schema === '*').map(meta => meta.tableName);
|
|
235
253
|
fromSchema.prune(options.schema, wildcardSchemaTables);
|
|
236
254
|
toSchema.prune(options.schema, wildcardSchemaTables);
|
|
237
255
|
return { fromSchema, toSchema };
|
|
@@ -300,11 +318,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
300
318
|
for (const check of newTable.getChecks()) {
|
|
301
319
|
this.append(sql, this.helper.createCheck(newTable, check));
|
|
302
320
|
}
|
|
321
|
+
for (const trigger of newTable.getTriggers()) {
|
|
322
|
+
this.append(sql, this.helper.createTrigger(newTable, trigger));
|
|
323
|
+
}
|
|
303
324
|
this.append(ret, sql, true);
|
|
304
325
|
}
|
|
305
326
|
}
|
|
306
327
|
if (options.dropTables && !options.safe) {
|
|
307
328
|
for (const table of Object.values(schemaDiff.removedTables)) {
|
|
329
|
+
// Drop triggers before the table so driver-specific cleanup runs (e.g. PostgreSQL function removal)
|
|
330
|
+
for (const trigger of table.getTriggers()) {
|
|
331
|
+
this.append(ret, this.helper.dropTrigger(table, trigger));
|
|
332
|
+
}
|
|
308
333
|
this.append(ret, this.helper.dropTableIfExists(table.name, table.schema));
|
|
309
334
|
}
|
|
310
335
|
if (Utils.hasObjectKeys(schemaDiff.removedTables)) {
|
|
@@ -340,6 +365,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
340
365
|
for (const view of sortedChangedViews) {
|
|
341
366
|
this.appendViewCreation(ret, view);
|
|
342
367
|
}
|
|
368
|
+
if (options.dropTables && !options.safe) {
|
|
369
|
+
for (const routine of Object.values(schemaDiff.removedRoutines)) {
|
|
370
|
+
this.append(ret, this.helper.dropRoutine(routine), true);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const change of Object.values(schemaDiff.changedRoutines)) {
|
|
374
|
+
this.append(ret, this.helper.dropRoutine(change.from), true);
|
|
375
|
+
this.append(ret, this.helper.createRoutine(change.to), true);
|
|
376
|
+
}
|
|
377
|
+
for (const routine of Object.values(schemaDiff.newRoutines)) {
|
|
378
|
+
this.append(ret, this.helper.createRoutine(routine), true);
|
|
379
|
+
}
|
|
343
380
|
return this.wrapSchema(ret, options);
|
|
344
381
|
}
|
|
345
382
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { splitCommaSeparatedIdentifiers, type EntityMetadata, type EntityPartitionBy } from '@mikro-orm/core';
|
|
2
|
+
import type { TablePartitioning } from '../typings.js';
|
|
3
|
+
export { splitCommaSeparatedIdentifiers };
|
|
4
|
+
/** @internal */
|
|
5
|
+
export declare function normalizePartitionDefinition(value: string): string;
|
|
6
|
+
/** @internal */
|
|
7
|
+
export declare function normalizePartitionBound(value: string): string;
|
|
8
|
+
/** @internal */
|
|
9
|
+
export declare const getTablePartitioning: (meta: EntityMetadata, tableSchema: string | undefined, quoteIdentifier?: (id: string) => string) => TablePartitioning | undefined;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export declare const diffPartitioning: (from: TablePartitioning | undefined, to: TablePartitioning | undefined, defaultSchema: string | undefined) => boolean;
|
|
12
|
+
/** @internal */
|
|
13
|
+
export declare const toEntityPartitionBy: (partitioning: TablePartitioning | undefined, parentTableName?: string, parentSchema?: string) => EntityPartitionBy | undefined;
|