@mikro-orm/sql 7.1.0-dev.2 → 7.1.0-dev.21
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 +25 -1
- package/AbstractSqlDriver.js +315 -15
- package/PivotCollectionPersister.js +13 -2
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- 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 +9 -3
- package/dialects/mysql/MySqlSchemaHelper.js +102 -4
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +2 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +21 -3
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +21 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +200 -4
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +8 -2
- package/dialects/sqlite/SqliteSchemaHelper.js +131 -19
- package/package.json +3 -3
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/NativeQueryBuilder.d.ts +6 -0
- package/query/NativeQueryBuilder.js +16 -1
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +77 -0
- package/query/QueryBuilder.js +170 -6
- package/schema/DatabaseSchema.js +26 -4
- package/schema/DatabaseTable.d.ts +13 -1
- package/schema/DatabaseTable.js +171 -31
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +86 -1
- package/schema/SchemaHelper.d.ts +51 -1
- package/schema/SchemaHelper.js +192 -3
- package/schema/SqlSchemaGenerator.js +7 -0
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +32 -1
package/schema/DatabaseTable.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DecimalType, EntitySchema, isRaw, ReferenceKind, t, Type, UnknownType, Utils, } from '@mikro-orm/core';
|
|
2
|
+
import { toEntityPartitionBy } from './partitioning.js';
|
|
2
3
|
/**
|
|
3
4
|
* @internal
|
|
4
5
|
*/
|
|
@@ -8,10 +9,12 @@ export class DatabaseTable {
|
|
|
8
9
|
#columns = {};
|
|
9
10
|
#indexes = [];
|
|
10
11
|
#checks = [];
|
|
12
|
+
#triggers = [];
|
|
11
13
|
#foreignKeys = {};
|
|
12
14
|
#platform;
|
|
13
15
|
nativeEnums = {}; // for postgres
|
|
14
16
|
comment;
|
|
17
|
+
partitioning;
|
|
15
18
|
constructor(platform, name, schema) {
|
|
16
19
|
this.name = name;
|
|
17
20
|
this.schema = schema;
|
|
@@ -35,6 +38,16 @@ export class DatabaseTable {
|
|
|
35
38
|
getChecks() {
|
|
36
39
|
return this.#checks;
|
|
37
40
|
}
|
|
41
|
+
getPartitioning() {
|
|
42
|
+
return this.partitioning;
|
|
43
|
+
}
|
|
44
|
+
/** @internal */
|
|
45
|
+
setPartitioning(partitioning) {
|
|
46
|
+
this.partitioning = partitioning;
|
|
47
|
+
}
|
|
48
|
+
getTriggers() {
|
|
49
|
+
return this.#triggers;
|
|
50
|
+
}
|
|
38
51
|
/** @internal */
|
|
39
52
|
setIndexes(indexes) {
|
|
40
53
|
this.#indexes = indexes;
|
|
@@ -44,6 +57,10 @@ export class DatabaseTable {
|
|
|
44
57
|
this.#checks = checks;
|
|
45
58
|
}
|
|
46
59
|
/** @internal */
|
|
60
|
+
setTriggers(triggers) {
|
|
61
|
+
this.#triggers = triggers;
|
|
62
|
+
}
|
|
63
|
+
/** @internal */
|
|
47
64
|
setForeignKeys(fks) {
|
|
48
65
|
this.#foreignKeys = fks;
|
|
49
66
|
}
|
|
@@ -51,6 +68,7 @@ export class DatabaseTable {
|
|
|
51
68
|
this.#indexes = indexes;
|
|
52
69
|
this.#checks = checks;
|
|
53
70
|
this.#foreignKeys = fks;
|
|
71
|
+
const helper = this.#platform.getSchemaHelper();
|
|
54
72
|
this.#columns = cols.reduce((o, v) => {
|
|
55
73
|
const index = indexes.filter(i => i.columnNames[0] === v.name);
|
|
56
74
|
v.primary = v.primary || pks.includes(v.name);
|
|
@@ -59,6 +77,11 @@ export class DatabaseTable {
|
|
|
59
77
|
v.mappedType = this.#platform.getMappedType(type);
|
|
60
78
|
v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
|
|
61
79
|
v.enumItems ??= enums[v.name] || [];
|
|
80
|
+
// recover length from the declared type so introspection matches `addColumnFromProperty`;
|
|
81
|
+
// scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
|
|
82
|
+
if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
|
|
83
|
+
v.length = helper.inferLengthFromColumnType(v.type);
|
|
84
|
+
}
|
|
62
85
|
o[v.name] = v;
|
|
63
86
|
return o;
|
|
64
87
|
}, {});
|
|
@@ -68,7 +91,9 @@ export class DatabaseTable {
|
|
|
68
91
|
}
|
|
69
92
|
addColumnFromProperty(prop, meta, config) {
|
|
70
93
|
prop.fieldNames?.forEach((field, idx) => {
|
|
71
|
-
|
|
94
|
+
// numeric enums fall through to the underlying numeric type — no platform emits a CHECK we could parse back
|
|
95
|
+
const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
|
|
96
|
+
const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
|
|
72
97
|
const mappedType = this.#platform.getMappedType(type);
|
|
73
98
|
if (mappedType instanceof DecimalType) {
|
|
74
99
|
const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
|
|
@@ -177,6 +202,7 @@ export class DatabaseTable {
|
|
|
177
202
|
const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames } = this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
|
|
178
203
|
const name = namingStrategy.getEntityName(this.name, this.schema);
|
|
179
204
|
const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment });
|
|
205
|
+
schema.meta.partitionBy = toEntityPartitionBy(this.partitioning, this.name, this.schema);
|
|
180
206
|
const compositeFkIndexes = {};
|
|
181
207
|
const compositeFkUniques = {};
|
|
182
208
|
const potentiallyUnmappedIndexes = this.#indexes.filter(index => !index.primary && // Skip primary index. Whether it's in use by scalar column or FK, it's already mapped.
|
|
@@ -196,6 +222,7 @@ export class DatabaseTable {
|
|
|
196
222
|
name: index.keyName,
|
|
197
223
|
deferMode: index.deferMode,
|
|
198
224
|
expression: index.expression,
|
|
225
|
+
where: index.where,
|
|
199
226
|
// Advanced index options - convert column names to property names
|
|
200
227
|
columns: index.columns?.map(col => ({
|
|
201
228
|
...col,
|
|
@@ -226,7 +253,7 @@ export class DatabaseTable {
|
|
|
226
253
|
index.invisible ||
|
|
227
254
|
index.disabled ||
|
|
228
255
|
index.clustered;
|
|
229
|
-
const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
|
|
256
|
+
const isTrivial = !index.deferMode && !index.expression && !index.where && !hasAdvancedOptions;
|
|
230
257
|
if (isTrivial) {
|
|
231
258
|
// Index is for FK. Map to the FK prop and move on.
|
|
232
259
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -598,6 +625,12 @@ export class DatabaseTable {
|
|
|
598
625
|
hasCheck(checkName) {
|
|
599
626
|
return !!this.getCheck(checkName);
|
|
600
627
|
}
|
|
628
|
+
getTrigger(triggerName) {
|
|
629
|
+
return this.#triggers.find(t => t.name === triggerName);
|
|
630
|
+
}
|
|
631
|
+
hasTrigger(triggerName) {
|
|
632
|
+
return !!this.getTrigger(triggerName);
|
|
633
|
+
}
|
|
601
634
|
getPrimaryKey() {
|
|
602
635
|
return this.#indexes.find(i => i.primary);
|
|
603
636
|
}
|
|
@@ -862,16 +895,25 @@ export class DatabaseTable {
|
|
|
862
895
|
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
|
|
863
896
|
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`);
|
|
864
897
|
}
|
|
898
|
+
// The `expression` escape hatch takes the full index definition as raw SQL; combining it
|
|
899
|
+
// with `where` is dialect-dependent (PG/Oracle/MySQL drop `where`, MSSQL appends it) so we
|
|
900
|
+
// reject the combination up-front and ask users to inline the predicate into `expression`.
|
|
901
|
+
if (index.expression && index.where != null) {
|
|
902
|
+
throw new Error(`Index '${name}' on entity '${meta.className}': cannot combine \`expression\` with \`where\` — inline the WHERE clause into the \`expression\` escape hatch, or drop \`expression\` and use structured \`properties\` + \`where\`.`);
|
|
903
|
+
}
|
|
904
|
+
const where = this.processIndexWhere(index.where, meta);
|
|
865
905
|
this.#indexes.push({
|
|
866
906
|
keyName: name,
|
|
867
907
|
columnNames: properties,
|
|
868
908
|
composite: properties.length > 1,
|
|
869
|
-
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
870
|
-
|
|
909
|
+
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them.
|
|
910
|
+
// Partial indexes (`where`) must use CREATE [UNIQUE] INDEX form — constraints can't carry predicates.
|
|
911
|
+
constraint: type !== 'index' && !properties.some((d) => d.includes('.')) && !where,
|
|
871
912
|
primary: type === 'primary',
|
|
872
913
|
unique: type !== 'index',
|
|
873
914
|
type: index.type,
|
|
874
915
|
expression: this.processIndexExpression(name, index.expression, meta),
|
|
916
|
+
where,
|
|
875
917
|
options: index.options,
|
|
876
918
|
deferMode: index.deferMode,
|
|
877
919
|
columns,
|
|
@@ -882,56 +924,154 @@ export class DatabaseTable {
|
|
|
882
924
|
clustered: index.clustered,
|
|
883
925
|
});
|
|
884
926
|
}
|
|
927
|
+
processIndexWhere(where, meta) {
|
|
928
|
+
if (where == null) {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
// The driver is always an `AbstractSqlDriver` here — `DatabaseTable` is only instantiated
|
|
932
|
+
// by SQL-side schema code, so the platform's config-bound driver is guaranteed to be one.
|
|
933
|
+
const driver = this.#platform.getConfig().getDriver();
|
|
934
|
+
return driver.renderPartialIndexWhere(meta.class, where);
|
|
935
|
+
}
|
|
885
936
|
addCheck(check) {
|
|
886
937
|
this.#checks.push(check);
|
|
887
938
|
}
|
|
939
|
+
addTrigger(trigger) {
|
|
940
|
+
this.#triggers.push(trigger);
|
|
941
|
+
}
|
|
888
942
|
toJSON() {
|
|
889
943
|
const columns = this.#columns;
|
|
890
|
-
|
|
944
|
+
// locale-independent comparison so the snapshot is stable across machines
|
|
945
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
946
|
+
const sortedColumnKeys = Utils.keys(columns).sort(byString);
|
|
947
|
+
// mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
|
|
948
|
+
// so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
|
|
949
|
+
const primaryColumns = new Set();
|
|
950
|
+
const uniqueColumns = new Set();
|
|
951
|
+
for (const idx of this.#indexes) {
|
|
952
|
+
if (idx.primary) {
|
|
953
|
+
idx.columnNames.forEach(c => primaryColumns.add(c));
|
|
954
|
+
}
|
|
955
|
+
if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
|
|
956
|
+
uniqueColumns.add(idx.columnNames[0]);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
|
|
960
|
+
const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
|
|
961
|
+
mappedType instanceof t.smallint ||
|
|
962
|
+
mappedType instanceof t.tinyint ||
|
|
963
|
+
mappedType instanceof t.mediumint ||
|
|
964
|
+
mappedType instanceof t.bigint ||
|
|
965
|
+
mappedType instanceof t.float ||
|
|
966
|
+
mappedType instanceof t.double;
|
|
967
|
+
const supportsUnsigned = this.#platform.supportsUnsigned();
|
|
968
|
+
const columnsMapped = sortedColumnKeys.reduce((o, col) => {
|
|
891
969
|
const c = columns[col];
|
|
970
|
+
// omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
|
|
971
|
+
const rawType = c.type?.toLowerCase();
|
|
972
|
+
const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
|
|
973
|
+
const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
|
|
974
|
+
const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
|
|
892
975
|
const normalized = {
|
|
893
976
|
name: c.name,
|
|
894
|
-
type
|
|
895
|
-
unsigned: !!c.unsigned,
|
|
977
|
+
type,
|
|
978
|
+
unsigned: supportsUnsigned && !!c.unsigned,
|
|
896
979
|
autoincrement: !!c.autoincrement,
|
|
897
|
-
primary: !!c.primary,
|
|
980
|
+
primary: primaryColumns.has(c.name) || !!c.primary,
|
|
898
981
|
nullable: !!c.nullable,
|
|
899
|
-
unique: !!c.unique,
|
|
900
|
-
length: c.length
|
|
901
|
-
precision: c.precision ?? null,
|
|
902
|
-
scale: c.scale ?? null,
|
|
982
|
+
unique: uniqueColumns.has(c.name) || !!c.unique,
|
|
983
|
+
length: c.length || null,
|
|
984
|
+
precision: fixedPrecision ? null : (c.precision ?? null),
|
|
985
|
+
scale: fixedPrecision ? null : (c.scale ?? null),
|
|
903
986
|
default: c.default ?? null,
|
|
904
987
|
comment: c.comment ?? null,
|
|
905
988
|
enumItems: c.enumItems ?? [],
|
|
906
989
|
mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
|
|
907
990
|
};
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
|
|
919
|
-
}
|
|
920
|
-
if (c.defaultConstraint) {
|
|
921
|
-
normalized.defaultConstraint = c.defaultConstraint;
|
|
991
|
+
for (const field of [
|
|
992
|
+
'generated',
|
|
993
|
+
'nativeEnumName',
|
|
994
|
+
'extra',
|
|
995
|
+
'ignoreSchemaChanges',
|
|
996
|
+
'defaultConstraint',
|
|
997
|
+
]) {
|
|
998
|
+
if (c[field]) {
|
|
999
|
+
normalized[field] = c[field];
|
|
1000
|
+
}
|
|
922
1001
|
}
|
|
923
1002
|
o[col] = normalized;
|
|
924
1003
|
return o;
|
|
925
1004
|
}, {});
|
|
1005
|
+
const normalizeIndex = (idx) => {
|
|
1006
|
+
const out = {
|
|
1007
|
+
columnNames: idx.columnNames,
|
|
1008
|
+
composite: !!idx.composite,
|
|
1009
|
+
// PK indexes are always backed by a constraint — force it so postgres introspection matches
|
|
1010
|
+
constraint: !!idx.constraint || !!idx.primary,
|
|
1011
|
+
keyName: idx.keyName,
|
|
1012
|
+
primary: !!idx.primary,
|
|
1013
|
+
unique: !!idx.unique,
|
|
1014
|
+
};
|
|
1015
|
+
const optional = [
|
|
1016
|
+
'expression',
|
|
1017
|
+
'type',
|
|
1018
|
+
'deferMode',
|
|
1019
|
+
'columns',
|
|
1020
|
+
'include',
|
|
1021
|
+
'fillFactor',
|
|
1022
|
+
'invisible',
|
|
1023
|
+
'disabled',
|
|
1024
|
+
'clustered',
|
|
1025
|
+
];
|
|
1026
|
+
for (const field of optional) {
|
|
1027
|
+
if (idx[field] != null && idx[field] !== false) {
|
|
1028
|
+
out[field] = idx[field];
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return out;
|
|
1032
|
+
};
|
|
1033
|
+
const normalizeFk = (fk) => {
|
|
1034
|
+
const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
|
|
1035
|
+
// JSON.stringify drops undefined properties — let them through instead of guarding
|
|
1036
|
+
return {
|
|
1037
|
+
columnNames: fk.columnNames,
|
|
1038
|
+
constraintName: fk.constraintName,
|
|
1039
|
+
localTableName: fk.localTableName,
|
|
1040
|
+
referencedColumnNames: fk.referencedColumnNames,
|
|
1041
|
+
referencedTableName: fk.referencedTableName,
|
|
1042
|
+
updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
|
|
1043
|
+
deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
|
|
1044
|
+
deferMode: fk.deferMode,
|
|
1045
|
+
};
|
|
1046
|
+
};
|
|
1047
|
+
const normalizeCheck = (check) => {
|
|
1048
|
+
const out = { name: check.name };
|
|
1049
|
+
if (typeof check.expression === 'string') {
|
|
1050
|
+
out.expression = check.expression;
|
|
1051
|
+
}
|
|
1052
|
+
for (const field of ['definition', 'columnName']) {
|
|
1053
|
+
if (check[field]) {
|
|
1054
|
+
out[field] = check[field];
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return out;
|
|
1058
|
+
};
|
|
1059
|
+
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1060
|
+
const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
|
|
1061
|
+
const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
|
|
1062
|
+
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1063
|
+
.sort(([a], [b]) => byString(a, b))
|
|
1064
|
+
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
926
1065
|
return {
|
|
927
1066
|
name: this.name,
|
|
928
1067
|
schema: this.schema,
|
|
929
1068
|
columns: columnsMapped,
|
|
930
|
-
indexes:
|
|
931
|
-
checks:
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
comment
|
|
1069
|
+
indexes: sortedIndexes,
|
|
1070
|
+
checks: sortedChecks,
|
|
1071
|
+
triggers: sortedTriggers,
|
|
1072
|
+
foreignKeys: sortedForeignKeys,
|
|
1073
|
+
// emit `comment` even when unset so introspection (which always reads it) matches metadata
|
|
1074
|
+
comment: this.comment ?? null,
|
|
935
1075
|
};
|
|
936
1076
|
}
|
|
937
1077
|
}
|
|
@@ -69,6 +69,7 @@ export declare class SchemaComparator {
|
|
|
69
69
|
* @see https://github.com/mikro-orm/mikro-orm/issues/7308
|
|
70
70
|
*/
|
|
71
71
|
private diffViewExpression;
|
|
72
|
+
private diffTrigger;
|
|
72
73
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
73
74
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
74
75
|
private mapColumnToProperty;
|
|
@@ -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)) {
|
|
@@ -542,7 +597,8 @@ export class SchemaComparator {
|
|
|
542
597
|
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
|
543
598
|
*/
|
|
544
599
|
diffIndex(index1, index2) {
|
|
545
|
-
//
|
|
600
|
+
// Opaque raw expressions (`expression` escape hatch) and full-text indexes can't be
|
|
601
|
+
// compared structurally — fall back to name-only matching.
|
|
546
602
|
if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
|
|
547
603
|
return index1.keyName !== index2.keyName;
|
|
548
604
|
}
|
|
@@ -593,6 +649,11 @@ export class SchemaComparator {
|
|
|
593
649
|
if (!!index1.clustered !== !!index2.clustered) {
|
|
594
650
|
return false;
|
|
595
651
|
}
|
|
652
|
+
// Compare WHERE predicate of partial indexes structurally (whitespace/quoting/casing
|
|
653
|
+
// are normalized via the same helper used for check constraints).
|
|
654
|
+
if (this.diffExpression(index1.where ?? '', index2.where ?? '')) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
596
657
|
if (!index1.unique && !index1.primary) {
|
|
597
658
|
// this is a special case: If the current key is neither primary or unique, any unique or
|
|
598
659
|
// primary key will always have the same effect for the index and there cannot be any constraint
|
|
@@ -714,6 +775,30 @@ export class SchemaComparator {
|
|
|
714
775
|
}
|
|
715
776
|
return true;
|
|
716
777
|
}
|
|
778
|
+
diffTrigger(from, to) {
|
|
779
|
+
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
780
|
+
// trigger metadata, so skip diffing when the metadata side uses it.
|
|
781
|
+
if (to.expression) {
|
|
782
|
+
// Both sides have expression — compare the raw DDL directly
|
|
783
|
+
if (from.expression) {
|
|
784
|
+
return this.diffExpression(from.expression, to.expression);
|
|
785
|
+
}
|
|
786
|
+
// Only metadata side has expression — the raw DDL cannot be compared to
|
|
787
|
+
// introspected metadata. Changes to the expression value won't be detected;
|
|
788
|
+
// drop and recreate the trigger manually to apply expression changes.
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
if (from.timing !== to.timing || from.forEach !== to.forEach) {
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
if ([...from.events].sort().join(',') !== [...to.events].sort().join(',')) {
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
if ((from.when ?? '') !== (to.when ?? '')) {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
return this.diffExpression(from.body, to.body);
|
|
801
|
+
}
|
|
717
802
|
parseJsonDefault(defaultValue) {
|
|
718
803
|
/* v8 ignore next */
|
|
719
804
|
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. */
|
|
@@ -41,6 +41,46 @@ export declare abstract class SchemaHelper {
|
|
|
41
41
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
42
42
|
*/
|
|
43
43
|
protected getCreateIndexSuffix(_index: IndexDef): string;
|
|
44
|
+
/**
|
|
45
|
+
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
46
|
+
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
47
|
+
* with its own `getCreateIndexSQL` that never calls this, and MariaDB refuses the feature
|
|
48
|
+
* entirely via an override on `getIndexColumns`.
|
|
49
|
+
*/
|
|
50
|
+
protected getIndexWhereClause(index: IndexDef): string;
|
|
51
|
+
/**
|
|
52
|
+
* Wraps each indexed column in `(CASE WHEN <predicate> THEN <col> END)` for dialects that
|
|
53
|
+
* emulate partial indexes via functional indexes (MySQL/MariaDB/Oracle). Combined with NULL
|
|
54
|
+
* being treated as distinct in unique indexes, this enforces uniqueness only where the
|
|
55
|
+
* predicate holds. Throws if combined with the advanced `columns` option.
|
|
56
|
+
*/
|
|
57
|
+
protected emulatePartialIndexColumns(index: IndexDef): string;
|
|
58
|
+
/**
|
|
59
|
+
* Strips `<col> IS NOT NULL` clauses (with the dialect's identifier quoting) from an
|
|
60
|
+
* introspected partial-index predicate when the column matches one of the index's own
|
|
61
|
+
* columns. MikroORM auto-emits this guard for unique indexes on nullable columns
|
|
62
|
+
* (MSSQL, Oracle) — it's an internal artifact, not user intent.
|
|
63
|
+
*
|
|
64
|
+
* Strips at most one guard per column (the tail-most occurrence), matching how MikroORM
|
|
65
|
+
* appends a single guard per index column. This preserves user intent when they redundantly
|
|
66
|
+
* include the same `<col> IS NOT NULL` in their predicate — the guard we added is removed,
|
|
67
|
+
* their copy survives.
|
|
68
|
+
*/
|
|
69
|
+
protected stripAutoNotNullFilter(filterDef: string, columnNames: string[], identifierPattern: RegExp): string;
|
|
70
|
+
/**
|
|
71
|
+
* Whether `[…]` is a quoted identifier (MSSQL convention). Other dialects either reuse
|
|
72
|
+
* `[` for array literals/constructors or never produce it in introspected predicates,
|
|
73
|
+
* so the default is `false` and the MSSQL helper opts in.
|
|
74
|
+
*/
|
|
75
|
+
protected get bracketQuotedIdentifiers(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Splits on top-level ` AND ` (case-insensitive), ignoring matches that sit inside string
|
|
78
|
+
* literals, quoted identifiers, or parenthesized groups — so a predicate like
|
|
79
|
+
* `'foo AND bar' = col` or `(a AND b) OR c` is not mis-split.
|
|
80
|
+
*/
|
|
81
|
+
protected splitTopLevelAnd(s: string): string[];
|
|
82
|
+
/** Returns true iff the leading `(` matches the trailing `)` (i.e. they wrap the whole string). */
|
|
83
|
+
protected isBalancedWrap(s: string): boolean;
|
|
44
84
|
/**
|
|
45
85
|
* Build the column list for an index, supporting advanced options like sort order, nulls ordering, and collation.
|
|
46
86
|
* Note: Prefix length is only supported by MySQL/MariaDB which override this method.
|
|
@@ -84,6 +124,16 @@ export declare abstract class SchemaHelper {
|
|
|
84
124
|
getReferencedTableName(referencedTableName: string, schema?: string): string;
|
|
85
125
|
createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
|
|
86
126
|
createCheck(table: DatabaseTable, check: CheckDef): string;
|
|
127
|
+
/**
|
|
128
|
+
* Generates SQL to create a database trigger on a table.
|
|
129
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
130
|
+
*/
|
|
131
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
132
|
+
/**
|
|
133
|
+
* Generates SQL to drop a database trigger from a table.
|
|
134
|
+
* Override in driver-specific helpers for custom DDL.
|
|
135
|
+
*/
|
|
136
|
+
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
87
137
|
/** @internal */
|
|
88
138
|
getTableName(table: string, schema?: string): string;
|
|
89
139
|
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
|