@mikro-orm/sql 7.1.0-dev.3 → 7.1.0-dev.31
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 +2 -2
- package/AbstractSqlDriver.d.ts +19 -1
- package/AbstractSqlDriver.js +215 -16
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.js +13 -2
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +13 -3
- package/dialects/mysql/MySqlSchemaHelper.js +145 -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 +9 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +72 -6
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +230 -5
- 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/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
- package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +4 -4
- package/plugin/transformer.d.ts +11 -3
- package/plugin/transformer.js +138 -29
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +36 -0
- package/query/QueryBuilder.js +63 -1
- package/schema/DatabaseSchema.js +26 -4
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +182 -31
- package/schema/SchemaComparator.d.ts +10 -0
- package/schema/SchemaComparator.js +104 -1
- package/schema/SchemaHelper.d.ts +63 -1
- package/schema/SchemaHelper.js +235 -6
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +16 -9
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +34 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { DatabaseTable } from './DatabaseTable.js';
|
|
3
|
+
import { diffPartitioning } from './partitioning.js';
|
|
3
4
|
/**
|
|
4
5
|
* Compares two Schemas and return an instance of SchemaDifference.
|
|
5
6
|
*/
|
|
@@ -176,14 +177,17 @@ export class SchemaComparator {
|
|
|
176
177
|
addedForeignKeys: {},
|
|
177
178
|
addedIndexes: {},
|
|
178
179
|
addedChecks: {},
|
|
180
|
+
addedTriggers: {},
|
|
179
181
|
changedColumns: {},
|
|
180
182
|
changedForeignKeys: {},
|
|
181
183
|
changedIndexes: {},
|
|
182
184
|
changedChecks: {},
|
|
185
|
+
changedTriggers: {},
|
|
183
186
|
removedColumns: {},
|
|
184
187
|
removedForeignKeys: {},
|
|
185
188
|
removedIndexes: {},
|
|
186
189
|
removedChecks: {},
|
|
190
|
+
removedTriggers: {},
|
|
187
191
|
renamedColumns: {},
|
|
188
192
|
renamedIndexes: {},
|
|
189
193
|
fromTable,
|
|
@@ -197,6 +201,17 @@ export class SchemaComparator {
|
|
|
197
201
|
});
|
|
198
202
|
changes++;
|
|
199
203
|
}
|
|
204
|
+
if (diffPartitioning(fromTable.getPartitioning(), toTable.getPartitioning(), this.#platform.getDefaultSchemaName())) {
|
|
205
|
+
tableDifferences.changedPartitioning = {
|
|
206
|
+
from: fromTable.getPartitioning(),
|
|
207
|
+
to: toTable.getPartitioning(),
|
|
208
|
+
};
|
|
209
|
+
this.log(`table partitioning changed for ${tableDifferences.name}`, {
|
|
210
|
+
fromPartitioning: fromTable.getPartitioning(),
|
|
211
|
+
toPartitioning: toTable.getPartitioning(),
|
|
212
|
+
});
|
|
213
|
+
changes++;
|
|
214
|
+
}
|
|
200
215
|
const fromTableColumns = fromTable.getColumns();
|
|
201
216
|
const toTableColumns = toTable.getColumns();
|
|
202
217
|
// See if all the columns in "from" table exist in "to" table
|
|
@@ -263,6 +278,19 @@ export class SchemaComparator {
|
|
|
263
278
|
if (!this.diffIndex(index, toTableIndex)) {
|
|
264
279
|
continue;
|
|
265
280
|
}
|
|
281
|
+
// Constraint-vs-index form mismatch needs drop+create with the OLD form's drop SQL,
|
|
282
|
+
// which `changedIndexes` (uses new form only) can't do. Primary keys stay on the
|
|
283
|
+
// changed path which emits `add primary key`.
|
|
284
|
+
if (!index.primary && !!index.constraint !== !!toTableIndex.constraint) {
|
|
285
|
+
tableDifferences.removedIndexes[index.keyName] = index;
|
|
286
|
+
tableDifferences.addedIndexes[index.keyName] = toTableIndex;
|
|
287
|
+
this.log(`index ${index.keyName} changed form in table ${tableDifferences.name}`, {
|
|
288
|
+
fromTableIndex: index,
|
|
289
|
+
toTableIndex,
|
|
290
|
+
});
|
|
291
|
+
changes += 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
266
294
|
tableDifferences.changedIndexes[index.keyName] = toTableIndex;
|
|
267
295
|
this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
|
|
268
296
|
fromTableIndex: index,
|
|
@@ -309,6 +337,33 @@ export class SchemaComparator {
|
|
|
309
337
|
tableDifferences.changedChecks[check.name] = toTableCheck;
|
|
310
338
|
changes++;
|
|
311
339
|
}
|
|
340
|
+
const fromTableTriggers = fromTable.getTriggers();
|
|
341
|
+
const toTableTriggers = toTable.getTriggers();
|
|
342
|
+
for (const trigger of toTableTriggers) {
|
|
343
|
+
if (fromTable.hasTrigger(trigger.name)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
tableDifferences.addedTriggers[trigger.name] = trigger;
|
|
347
|
+
this.log(`trigger ${trigger.name} added to table ${tableDifferences.name}`, { trigger });
|
|
348
|
+
changes++;
|
|
349
|
+
}
|
|
350
|
+
for (const trigger of fromTableTriggers) {
|
|
351
|
+
if (!toTable.hasTrigger(trigger.name)) {
|
|
352
|
+
tableDifferences.removedTriggers[trigger.name] = trigger;
|
|
353
|
+
this.log(`trigger ${trigger.name} removed from table ${tableDifferences.name}`);
|
|
354
|
+
changes++;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const toTableTrigger = toTable.getTrigger(trigger.name);
|
|
358
|
+
if (this.diffTrigger(trigger, toTableTrigger)) {
|
|
359
|
+
this.log(`trigger ${trigger.name} changed in table ${tableDifferences.name}`, {
|
|
360
|
+
fromTableTrigger: trigger,
|
|
361
|
+
toTableTrigger,
|
|
362
|
+
});
|
|
363
|
+
tableDifferences.changedTriggers[trigger.name] = toTableTrigger;
|
|
364
|
+
changes++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
312
367
|
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
|
313
368
|
const toForeignKeys = { ...toTable.getForeignKeys() };
|
|
314
369
|
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
|
@@ -516,6 +571,11 @@ export class SchemaComparator {
|
|
|
516
571
|
log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
517
572
|
changedProperties.add('comment');
|
|
518
573
|
}
|
|
574
|
+
if (!(fromColumn.ignoreSchemaChanges?.includes('collation') || toColumn.ignoreSchemaChanges?.includes('collation')) &&
|
|
575
|
+
this.diffCollation(fromColumn.collation, toColumn.collation, fromTable.collation)) {
|
|
576
|
+
log(`'collation' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
577
|
+
changedProperties.add('collation');
|
|
578
|
+
}
|
|
519
579
|
const isNonNativeEnumArray = !(fromColumn.nativeEnumName || toColumn.nativeEnumName) &&
|
|
520
580
|
(fromColumn.mappedType instanceof ArrayType || toColumn.mappedType instanceof ArrayType);
|
|
521
581
|
if (!isNonNativeEnumArray && this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
|
|
@@ -537,12 +597,26 @@ export class SchemaComparator {
|
|
|
537
597
|
// eslint-disable-next-line eqeqeq
|
|
538
598
|
return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
|
|
539
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* `from` is the introspected DB state, `to` is the target metadata. A column-level `COLLATE`
|
|
602
|
+
* clause naming the table/database default is just verbose syntax for inheriting that default,
|
|
603
|
+
* so both sides are normalized — anything matching `tableDefault` collapses to `undefined` and
|
|
604
|
+
* compares equal to "no explicit collation". Comparison is case-insensitive on dialects that
|
|
605
|
+
* treat collation identifiers as case-insensitive (MySQL/MSSQL/SQLite); PostgreSQL's
|
|
606
|
+
* `pg_collation.collname` is case-sensitive and is compared verbatim.
|
|
607
|
+
*/
|
|
608
|
+
diffCollation(fromCollation, toCollation, tableDefault) {
|
|
609
|
+
const fold = this.#platform.caseInsensitiveCollationNames() ? (s) => s.toLowerCase() : (s) => s;
|
|
610
|
+
const norm = (c) => c && tableDefault && fold(c) === fold(tableDefault) ? undefined : c == null ? undefined : fold(c);
|
|
611
|
+
return norm(fromCollation) !== norm(toCollation);
|
|
612
|
+
}
|
|
540
613
|
/**
|
|
541
614
|
* Finds the difference between the indexes index1 and index2.
|
|
542
615
|
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
|
543
616
|
*/
|
|
544
617
|
diffIndex(index1, index2) {
|
|
545
|
-
//
|
|
618
|
+
// Opaque raw expressions (`expression` escape hatch) and full-text indexes can't be
|
|
619
|
+
// compared structurally — fall back to name-only matching.
|
|
546
620
|
if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
|
|
547
621
|
return index1.keyName !== index2.keyName;
|
|
548
622
|
}
|
|
@@ -593,6 +667,11 @@ export class SchemaComparator {
|
|
|
593
667
|
if (!!index1.clustered !== !!index2.clustered) {
|
|
594
668
|
return false;
|
|
595
669
|
}
|
|
670
|
+
// Compare WHERE predicate of partial indexes structurally (whitespace/quoting/casing
|
|
671
|
+
// are normalized via the same helper used for check constraints).
|
|
672
|
+
if (this.diffExpression(index1.where ?? '', index2.where ?? '')) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
596
675
|
if (!index1.unique && !index1.primary) {
|
|
597
676
|
// this is a special case: If the current key is neither primary or unique, any unique or
|
|
598
677
|
// primary key will always have the same effect for the index and there cannot be any constraint
|
|
@@ -714,6 +793,30 @@ export class SchemaComparator {
|
|
|
714
793
|
}
|
|
715
794
|
return true;
|
|
716
795
|
}
|
|
796
|
+
diffTrigger(from, to) {
|
|
797
|
+
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
798
|
+
// trigger metadata, so skip diffing when the metadata side uses it.
|
|
799
|
+
if (to.expression) {
|
|
800
|
+
// Both sides have expression — compare the raw DDL directly
|
|
801
|
+
if (from.expression) {
|
|
802
|
+
return this.diffExpression(from.expression, to.expression);
|
|
803
|
+
}
|
|
804
|
+
// Only metadata side has expression — the raw DDL cannot be compared to
|
|
805
|
+
// introspected metadata. Changes to the expression value won't be detected;
|
|
806
|
+
// drop and recreate the trigger manually to apply expression changes.
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
if (from.timing !== to.timing || from.forEach !== to.forEach) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
if ([...from.events].sort().join(',') !== [...to.events].sort().join(',')) {
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
if ((from.when ?? '') !== (to.when ?? '')) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
return this.diffExpression(from.body, to.body);
|
|
819
|
+
}
|
|
717
820
|
parseJsonDefault(defaultValue) {
|
|
718
821
|
/* v8 ignore next */
|
|
719
822
|
if (!defaultValue) {
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 } from '../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from './DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from './DatabaseTable.js';
|
|
7
7
|
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
|
|
@@ -16,6 +16,14 @@ export declare abstract class SchemaHelper {
|
|
|
16
16
|
enableForeignKeysSQL(): string;
|
|
17
17
|
/** Returns SQL to append to schema migration scripts (e.g., re-enabling FK checks). */
|
|
18
18
|
getSchemaEnd(disableForeignKeys?: boolean): string;
|
|
19
|
+
/** Sets the current schema for the session (e.g. `SET search_path`). */
|
|
20
|
+
getSetSchemaSQL(_schema: string): string;
|
|
21
|
+
/** Whether the driver supports setting a runtime schema per migration run. */
|
|
22
|
+
supportsMigrationSchema(): boolean;
|
|
23
|
+
/** Restores the session's schema to the connection's default after a migration. */
|
|
24
|
+
getResetSchemaSQL(_defaultSchema: string): string;
|
|
25
|
+
/** Returns `undefined` for schemaless drivers, throws for drivers that have schemas but no session switch. */
|
|
26
|
+
resolveMigrationSchema(schema: string | undefined): string | undefined;
|
|
19
27
|
finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
|
|
20
28
|
appendComments(table: DatabaseTable): string[];
|
|
21
29
|
supportsSchemaConstraints(): boolean;
|
|
@@ -31,6 +39,8 @@ export declare abstract class SchemaHelper {
|
|
|
31
39
|
getListTablesSQL(): string;
|
|
32
40
|
/** Retrieves all tables from the database. */
|
|
33
41
|
getAllTables(connection: AbstractSqlConnection, schemas?: string[], ctx?: Transaction): Promise<Table[]>;
|
|
42
|
+
/** Checks whether a specific table exists in a given schema (not the connection's current schema). */
|
|
43
|
+
tableExists(connection: AbstractSqlConnection, tableName: string, schemaName: string | undefined, ctx?: Transaction): Promise<boolean>;
|
|
34
44
|
getListViewsSQL(): string;
|
|
35
45
|
loadViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string, ctx?: Transaction): Promise<void>;
|
|
36
46
|
/** Returns SQL to rename a column in a table. */
|
|
@@ -41,6 +51,46 @@ export declare abstract class SchemaHelper {
|
|
|
41
51
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
42
52
|
*/
|
|
43
53
|
protected getCreateIndexSuffix(_index: IndexDef): string;
|
|
54
|
+
/**
|
|
55
|
+
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
56
|
+
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
57
|
+
* with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
|
|
58
|
+
* entirely via an override on `getIndexColumns`.
|
|
59
|
+
*/
|
|
60
|
+
protected getIndexWhereClause(index: IndexDef): string;
|
|
61
|
+
/**
|
|
62
|
+
* Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
|
|
63
|
+
* emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
|
|
64
|
+
* being treated as distinct in unique indexes, this enforces uniqueness only where the
|
|
65
|
+
* predicate holds. Throws if combined with the advanced `columns` option.
|
|
66
|
+
*/
|
|
67
|
+
protected emulatePartialIndexColumns(index: IndexDef): string;
|
|
68
|
+
/**
|
|
69
|
+
* Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
|
|
70
|
+
* introspected partial-index predicate when the column matches one of the index's own
|
|
71
|
+
* columns. MikroORM auto-emits this guard for unique indexes on nullable columns
|
|
72
|
+
* (MSSQL, Oracle) — it's an internal artifact, not user intent.
|
|
73
|
+
*
|
|
74
|
+
* Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
|
|
75
|
+
* appends a single guard per index column. This preserves user intent when they redundantly
|
|
76
|
+
* include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
|
|
77
|
+
* their copy survives.
|
|
78
|
+
*/
|
|
79
|
+
protected stripAutoNotNullFilter(filterDef: string, columnNames: string[], identifierPattern: RegExp): string;
|
|
80
|
+
/**
|
|
81
|
+
* Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
|
|
82
|
+
* `[` for array literals/constructors or never produce it in introspected predicates,
|
|
83
|
+
* so the default is `false` and the MSSQL helper opts in.
|
|
84
|
+
*/
|
|
85
|
+
protected get bracketQuotedIdentifiers(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
|
|
88
|
+
* literals, quoted identifiers, or parenthesized groups — so a predicate like
|
|
89
|
+
* `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
|
|
90
|
+
*/
|
|
91
|
+
protected splitTopLevelAnd(s: string): string[];
|
|
92
|
+
/** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
|
|
93
|
+
protected isBalancedWrap(s: string): boolean;
|
|
44
94
|
/**
|
|
45
95
|
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
46
96
|
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
@@ -57,6 +107,8 @@ export declare abstract class SchemaHelper {
|
|
|
57
107
|
hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
|
|
58
108
|
castColumn(name: string, type: string): string;
|
|
59
109
|
alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
|
|
110
|
+
/** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
|
|
111
|
+
protected getCollateSQL(collation: string): string;
|
|
60
112
|
createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
|
|
61
113
|
getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
62
114
|
getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
@@ -84,6 +136,16 @@ export declare abstract class SchemaHelper {
|
|
|
84
136
|
getReferencedTableName(referencedTableName: string, schema?: string): string;
|
|
85
137
|
createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
|
|
86
138
|
createCheck(table: DatabaseTable, check: CheckDef): string;
|
|
139
|
+
/**
|
|
140
|
+
* Generates SQL to create a database trigger on a table.
|
|
141
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
142
|
+
*/
|
|
143
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
144
|
+
/**
|
|
145
|
+
* Generates SQL to drop a database trigger from a table.
|
|
146
|
+
* Override in driver-specific helpers for custom DDL.
|
|
147
|
+
*/
|
|
148
|
+
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
87
149
|
/** @internal */
|
|
88
150
|
getTableName(table: string, schema?: string): string;
|
|
89
151
|
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -27,6 +27,32 @@ export class SchemaHelper {
|
|
|
27
27
|
}
|
|
28
28
|
return '';
|
|
29
29
|
}
|
|
30
|
+
/** Sets the current schema for the session (e.g. `SET search_path`). */
|
|
31
|
+
getSetSchemaSQL(_schema) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
/** Whether the driver supports setting a runtime schema per migration run. */
|
|
35
|
+
supportsMigrationSchema() {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/** Restores the session's schema to the connection's default after a migration. */
|
|
39
|
+
getResetSchemaSQL(_defaultSchema) {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
/** Returns `undefined` for schemaless drivers, throws for drivers that have schemas but no session switch. */
|
|
43
|
+
resolveMigrationSchema(schema) {
|
|
44
|
+
if (!schema) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (this.supportsMigrationSchema()) {
|
|
48
|
+
return schema;
|
|
49
|
+
}
|
|
50
|
+
if (!this.platform.supportsSchemas()) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const driverName = this.platform.constructor.name.replace(/Platform$/, 'Driver');
|
|
54
|
+
throw new Error(`Runtime schema for migrations is not supported by the ${driverName}`);
|
|
55
|
+
}
|
|
30
56
|
finalizeTable(table, charset, collate) {
|
|
31
57
|
return '';
|
|
32
58
|
}
|
|
@@ -42,7 +68,7 @@ export class SchemaHelper {
|
|
|
42
68
|
}
|
|
43
69
|
inferLengthFromColumnType(type) {
|
|
44
70
|
const match = /^\w+\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
|
|
45
|
-
if (
|
|
71
|
+
if (match?.[1] == null) {
|
|
46
72
|
return;
|
|
47
73
|
}
|
|
48
74
|
return +match[1];
|
|
@@ -75,6 +101,13 @@ export class SchemaHelper {
|
|
|
75
101
|
async getAllTables(connection, schemas, ctx) {
|
|
76
102
|
return connection.execute(this.getListTablesSQL(), [], 'all', ctx);
|
|
77
103
|
}
|
|
104
|
+
/** Checks whether a specific table exists in a given schema (not the connection's current schema). */
|
|
105
|
+
async tableExists(connection, tableName, schemaName, ctx) {
|
|
106
|
+
const qv = (v) => this.platform.quoteValue(v ?? '');
|
|
107
|
+
const resolved = schemaName ?? this.platform.getDefaultSchemaName();
|
|
108
|
+
const rows = await connection.execute(`select 1 from information_schema.tables where table_schema = ${qv(resolved)} and table_name = ${qv(tableName)}`, [], 'all', ctx);
|
|
109
|
+
return rows.length > 0;
|
|
110
|
+
}
|
|
78
111
|
getListViewsSQL() {
|
|
79
112
|
throw new Error('Not supported by given driver');
|
|
80
113
|
}
|
|
@@ -110,7 +143,7 @@ export class SchemaHelper {
|
|
|
110
143
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
111
144
|
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
|
|
112
145
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
113
|
-
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${defer}`;
|
|
146
|
+
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${this.getIndexWhereClause(index)}${defer}`;
|
|
114
147
|
}
|
|
115
148
|
// Build column list with advanced options
|
|
116
149
|
const columns = this.getIndexColumns(index);
|
|
@@ -119,7 +152,7 @@ export class SchemaHelper {
|
|
|
119
152
|
if (index.include?.length) {
|
|
120
153
|
sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
|
|
121
154
|
}
|
|
122
|
-
return sql + this.getCreateIndexSuffix(index) + defer;
|
|
155
|
+
return sql + this.getCreateIndexSuffix(index) + this.getIndexWhereClause(index) + defer;
|
|
123
156
|
}
|
|
124
157
|
/**
|
|
125
158
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
@@ -127,6 +160,153 @@ export class SchemaHelper {
|
|
|
127
160
|
getCreateIndexSuffix(_index) {
|
|
128
161
|
return '';
|
|
129
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
165
|
+
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
166
|
+
* with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
|
|
167
|
+
* entirely via an override on `getIndexColumns`.
|
|
168
|
+
*/
|
|
169
|
+
getIndexWhereClause(index) {
|
|
170
|
+
return index.where ? ` where ${index.where}` : '';
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
|
|
174
|
+
* emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
|
|
175
|
+
* being treated as distinct in unique indexes, this enforces uniqueness only where the
|
|
176
|
+
* predicate holds. Throws if combined with the advanced `columns` option.
|
|
177
|
+
*/
|
|
178
|
+
emulatePartialIndexColumns(index) {
|
|
179
|
+
if (index.columns?.length) {
|
|
180
|
+
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\`).`);
|
|
181
|
+
}
|
|
182
|
+
const predicate = index.where;
|
|
183
|
+
return index.columnNames.map(c => `(case when ${predicate} then ${this.quote(c)} end)`).join(', ');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
|
|
187
|
+
* introspected partial-index predicate when the column matches one of the index's own
|
|
188
|
+
* columns. MikroORM auto-emits this guard for unique indexes on nullable columns
|
|
189
|
+
* (MSSQL, Oracle) — it's an internal artifact, not user intent.
|
|
190
|
+
*
|
|
191
|
+
* Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
|
|
192
|
+
* appends a single guard per index column. This preserves user intent when they redundantly
|
|
193
|
+
* include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
|
|
194
|
+
* their copy survives.
|
|
195
|
+
*/
|
|
196
|
+
stripAutoNotNullFilter(filterDef, columnNames, identifierPattern) {
|
|
197
|
+
// Peel off any number of balanced wrapping paren layers. Introspection sources differ
|
|
198
|
+
// (MSSQL `filter_definition` wraps once, Oracle `INDEX_EXPRESSIONS` typically not at all),
|
|
199
|
+
// and a user `where` round-tripped through a dialect that double-wraps would otherwise slip
|
|
200
|
+
// past the auto-NOT-NULL recognizer below.
|
|
201
|
+
let inner = filterDef.trim();
|
|
202
|
+
while (inner.startsWith('(') && inner.endsWith(')') && this.isBalancedWrap(inner)) {
|
|
203
|
+
inner = inner.slice(1, -1).trim();
|
|
204
|
+
}
|
|
205
|
+
const clauses = this.splitTopLevelAnd(inner);
|
|
206
|
+
const autoCol = (clause) => {
|
|
207
|
+
let trimmed = clause.trim();
|
|
208
|
+
while (trimmed.startsWith('(') && trimmed.endsWith(')') && this.isBalancedWrap(trimmed)) {
|
|
209
|
+
trimmed = trimmed.slice(1, -1).trim();
|
|
210
|
+
}
|
|
211
|
+
const match = identifierPattern.exec(trimmed);
|
|
212
|
+
return match && columnNames.includes(match[1]) ? match[1] : null;
|
|
213
|
+
};
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
const kept = [];
|
|
216
|
+
for (let i = clauses.length - 1; i >= 0; i--) {
|
|
217
|
+
const col = autoCol(clauses[i]);
|
|
218
|
+
if (col && !seen.has(col)) {
|
|
219
|
+
seen.add(col);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
kept.unshift(clauses[i]);
|
|
223
|
+
}
|
|
224
|
+
return kept.join(' and ').trim();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
|
|
228
|
+
* `[` for array literals/constructors or never produce it in introspected predicates,
|
|
229
|
+
* so the default is `false` and the MSSQL helper opts in.
|
|
230
|
+
*/
|
|
231
|
+
get bracketQuotedIdentifiers() {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
|
|
236
|
+
* literals, quoted identifiers, or parenthesized groups — so a predicate like
|
|
237
|
+
* `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
|
|
238
|
+
*/
|
|
239
|
+
splitTopLevelAnd(s) {
|
|
240
|
+
const parts = [];
|
|
241
|
+
let depth = 0;
|
|
242
|
+
let quote = null;
|
|
243
|
+
let start = 0;
|
|
244
|
+
let i = 0;
|
|
245
|
+
while (i < s.length) {
|
|
246
|
+
const c = s[i];
|
|
247
|
+
if (quote) {
|
|
248
|
+
// Handle SQL's doubled-delimiter escape inside quoted strings/identifiers:
|
|
249
|
+
// `'` → `''`, `"` → `""`, `` ` `` → ```` `` ````, MSSQL `]` → `]]`.
|
|
250
|
+
if (c === quote && s[i + 1] === quote) {
|
|
251
|
+
i += 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (c === quote) {
|
|
255
|
+
quote = null;
|
|
256
|
+
}
|
|
257
|
+
i++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
261
|
+
quote = c;
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (c === '[' && this.bracketQuotedIdentifiers) {
|
|
266
|
+
quote = ']';
|
|
267
|
+
i++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (c === '(') {
|
|
271
|
+
depth++;
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (c === ')') {
|
|
276
|
+
depth--;
|
|
277
|
+
i++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (depth === 0 && /\s/.test(c)) {
|
|
281
|
+
const m = /^\s+and\s+/i.exec(s.slice(i));
|
|
282
|
+
if (m) {
|
|
283
|
+
parts.push(s.slice(start, i).trim());
|
|
284
|
+
i += m[0].length;
|
|
285
|
+
start = i;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
i++;
|
|
290
|
+
}
|
|
291
|
+
parts.push(s.slice(start).trim());
|
|
292
|
+
return parts.filter(p => p.length > 0);
|
|
293
|
+
}
|
|
294
|
+
/** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
|
|
295
|
+
isBalancedWrap(s) {
|
|
296
|
+
let depth = 0;
|
|
297
|
+
for (let i = 0; i < s.length; i++) {
|
|
298
|
+
if (s[i] === '(') {
|
|
299
|
+
depth++;
|
|
300
|
+
}
|
|
301
|
+
else if (s[i] === ')') {
|
|
302
|
+
depth--;
|
|
303
|
+
if (depth === 0 && i < s.length - 1) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return depth === 0;
|
|
309
|
+
}
|
|
130
310
|
/**
|
|
131
311
|
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
132
312
|
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
@@ -204,6 +384,12 @@ export class SchemaHelper {
|
|
|
204
384
|
for (const check of Object.values(diff.changedChecks)) {
|
|
205
385
|
ret.push(this.dropConstraint(diff.name, check.name));
|
|
206
386
|
}
|
|
387
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
388
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
389
|
+
}
|
|
390
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
391
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
392
|
+
}
|
|
207
393
|
/* v8 ignore next */
|
|
208
394
|
if (!safe && Object.values(diff.removedColumns).length > 0) {
|
|
209
395
|
ret.push(this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName));
|
|
@@ -228,7 +414,7 @@ export class SchemaHelper {
|
|
|
228
414
|
this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
|
|
229
415
|
}
|
|
230
416
|
for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
|
|
231
|
-
if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems'].some(t => changedProperties.has(t))) {
|
|
417
|
+
if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
|
|
232
418
|
continue; // will be handled via column update
|
|
233
419
|
}
|
|
234
420
|
ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
|
|
@@ -263,6 +449,12 @@ export class SchemaHelper {
|
|
|
263
449
|
for (const check of Object.values(diff.changedChecks)) {
|
|
264
450
|
ret.push(this.createCheck(diff.toTable, check));
|
|
265
451
|
}
|
|
452
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
453
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
454
|
+
}
|
|
455
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
456
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
457
|
+
}
|
|
266
458
|
if ('changedComment' in diff) {
|
|
267
459
|
ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
|
|
268
460
|
}
|
|
@@ -299,7 +491,7 @@ export class SchemaHelper {
|
|
|
299
491
|
if (changedProperties.has('default') && column.default == null) {
|
|
300
492
|
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} drop default`);
|
|
301
493
|
}
|
|
302
|
-
if (changedProperties.has('type')) {
|
|
494
|
+
if (changedProperties.has('type') || changedProperties.has('collation')) {
|
|
303
495
|
let type = column.type + (column.generated ? ` generated always as ${column.generated}` : '');
|
|
304
496
|
if (column.nativeEnumName) {
|
|
305
497
|
const parts = type.split('.');
|
|
@@ -311,7 +503,8 @@ export class SchemaHelper {
|
|
|
311
503
|
}
|
|
312
504
|
type = this.quote(type);
|
|
313
505
|
}
|
|
314
|
-
|
|
506
|
+
const collateClause = column.collation ? ` ${this.getCollateSQL(column.collation)}` : '';
|
|
507
|
+
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + collateClause + this.castColumn(column.name, type)}`);
|
|
315
508
|
}
|
|
316
509
|
if (changedProperties.has('default') && column.default != null) {
|
|
317
510
|
sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} set default ${column.default}`);
|
|
@@ -322,6 +515,11 @@ export class SchemaHelper {
|
|
|
322
515
|
}
|
|
323
516
|
return sql;
|
|
324
517
|
}
|
|
518
|
+
/** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
|
|
519
|
+
getCollateSQL(collation) {
|
|
520
|
+
this.platform.validateCollationName(collation);
|
|
521
|
+
return `collate ${collation}`;
|
|
522
|
+
}
|
|
325
523
|
createTableColumn(column, table, changedProperties) {
|
|
326
524
|
const compositePK = table.getPrimaryKey()?.composite;
|
|
327
525
|
const primaryKey = !changedProperties && !this.hasNonDefaultPrimaryKeyName(table);
|
|
@@ -329,6 +527,7 @@ export class SchemaHelper {
|
|
|
329
527
|
const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement;
|
|
330
528
|
const col = [this.quote(column.name), columnType];
|
|
331
529
|
Utils.runIfNotEmpty(() => col.push('unsigned'), column.unsigned && this.platform.supportsUnsigned());
|
|
530
|
+
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
332
531
|
Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
|
|
333
532
|
Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
|
|
334
533
|
Utils.runIfNotEmpty(() => col.push('auto_increment'), column.autoincrement);
|
|
@@ -521,6 +720,9 @@ export class SchemaHelper {
|
|
|
521
720
|
for (const check of table.getChecks()) {
|
|
522
721
|
this.append(ret, this.createCheck(table, check));
|
|
523
722
|
}
|
|
723
|
+
for (const trigger of table.getTriggers()) {
|
|
724
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
725
|
+
}
|
|
524
726
|
}
|
|
525
727
|
return ret;
|
|
526
728
|
}
|
|
@@ -600,6 +802,33 @@ export class SchemaHelper {
|
|
|
600
802
|
createCheck(table, check) {
|
|
601
803
|
return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
|
|
602
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Generates SQL to create a database trigger on a table.
|
|
807
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
808
|
+
*/
|
|
809
|
+
/* v8 ignore next 10 */
|
|
810
|
+
createTrigger(table, trigger) {
|
|
811
|
+
if (trigger.expression) {
|
|
812
|
+
return trigger.expression;
|
|
813
|
+
}
|
|
814
|
+
const timing = trigger.timing.toUpperCase();
|
|
815
|
+
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
816
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
817
|
+
const when = trigger.when ? ` when (${trigger.when})` : '';
|
|
818
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Generates SQL to drop a database trigger from a table.
|
|
822
|
+
* Override in driver-specific helpers for custom DDL.
|
|
823
|
+
*/
|
|
824
|
+
dropTrigger(table, trigger) {
|
|
825
|
+
if (trigger.events.length > 1) {
|
|
826
|
+
return trigger.events
|
|
827
|
+
.map(event => `drop trigger if exists ${this.quote(`${trigger.name}_${event}`)}`)
|
|
828
|
+
.join(';\n');
|
|
829
|
+
}
|
|
830
|
+
return `drop trigger if exists ${this.quote(trigger.name)}`;
|
|
831
|
+
}
|
|
603
832
|
/** @internal */
|
|
604
833
|
getTableName(table, schema) {
|
|
605
834
|
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>;
|