@mikro-orm/sql 7.1.0-dev.8 → 7.1.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 +15 -1
- package/AbstractSqlDriver.js +143 -26
- package/AbstractSqlPlatform.d.ts +15 -3
- package/AbstractSqlPlatform.js +25 -7
- package/PivotCollectionPersister.d.ts +2 -2
- package/PivotCollectionPersister.js +6 -1
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +48 -5
- package/SqlEntityManager.js +77 -7
- package/SqlMikroORM.d.ts +23 -0
- package/SqlMikroORM.js +23 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +3 -5
- package/dialects/mysql/BaseMySqlPlatform.js +6 -10
- package/dialects/mysql/MySqlSchemaHelper.d.ts +16 -3
- package/dialects/mysql/MySqlSchemaHelper.js +197 -49
- 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 +11 -5
- package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +269 -28
- 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 +2 -1
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +4 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +49 -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 +42 -1
- package/query/QueryBuilder.js +78 -7
- package/schema/DatabaseSchema.d.ts +29 -2
- package/schema/DatabaseSchema.js +131 -4
- package/schema/DatabaseTable.d.ts +14 -1
- package/schema/DatabaseTable.js +165 -32
- package/schema/SchemaComparator.d.ts +18 -0
- package/schema/SchemaComparator.js +196 -1
- package/schema/SchemaHelper.d.ts +67 -1
- package/schema/SchemaHelper.js +255 -25
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +40 -10
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +59 -5
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
|
*/
|
|
@@ -13,6 +14,14 @@ export class DatabaseTable {
|
|
|
13
14
|
#platform;
|
|
14
15
|
nativeEnums = {}; // for postgres
|
|
15
16
|
comment;
|
|
17
|
+
partitioning;
|
|
18
|
+
/**
|
|
19
|
+
* Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
|
|
20
|
+
* For MySQL/MariaDB this is the table collation; for PostgreSQL and MSSQL this is the database default;
|
|
21
|
+
* SQLite has no configurable default. Used by `SchemaComparator.diffCollation` to avoid flapping
|
|
22
|
+
* when a property explicitly names the default collation.
|
|
23
|
+
*/
|
|
24
|
+
collation;
|
|
16
25
|
constructor(platform, name, schema) {
|
|
17
26
|
this.name = name;
|
|
18
27
|
this.schema = schema;
|
|
@@ -36,6 +45,13 @@ export class DatabaseTable {
|
|
|
36
45
|
getChecks() {
|
|
37
46
|
return this.#checks;
|
|
38
47
|
}
|
|
48
|
+
getPartitioning() {
|
|
49
|
+
return this.partitioning;
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
setPartitioning(partitioning) {
|
|
53
|
+
this.partitioning = partitioning;
|
|
54
|
+
}
|
|
39
55
|
getTriggers() {
|
|
40
56
|
return this.#triggers;
|
|
41
57
|
}
|
|
@@ -59,6 +75,7 @@ export class DatabaseTable {
|
|
|
59
75
|
this.#indexes = indexes;
|
|
60
76
|
this.#checks = checks;
|
|
61
77
|
this.#foreignKeys = fks;
|
|
78
|
+
const helper = this.#platform.getSchemaHelper();
|
|
62
79
|
this.#columns = cols.reduce((o, v) => {
|
|
63
80
|
const index = indexes.filter(i => i.columnNames[0] === v.name);
|
|
64
81
|
v.primary = v.primary || pks.includes(v.name);
|
|
@@ -67,6 +84,11 @@ export class DatabaseTable {
|
|
|
67
84
|
v.mappedType = this.#platform.getMappedType(type);
|
|
68
85
|
v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
|
|
69
86
|
v.enumItems ??= enums[v.name] || [];
|
|
87
|
+
// recover length from the declared type so introspection matches `addColumnFromProperty`;
|
|
88
|
+
// scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
|
|
89
|
+
if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
|
|
90
|
+
v.length = helper.inferLengthFromColumnType(v.type);
|
|
91
|
+
}
|
|
70
92
|
o[v.name] = v;
|
|
71
93
|
return o;
|
|
72
94
|
}, {});
|
|
@@ -76,7 +98,9 @@ export class DatabaseTable {
|
|
|
76
98
|
}
|
|
77
99
|
addColumnFromProperty(prop, meta, config) {
|
|
78
100
|
prop.fieldNames?.forEach((field, idx) => {
|
|
79
|
-
|
|
101
|
+
// numeric enums fall through to the underlying numeric type — no platform emits a CHECK we could parse back
|
|
102
|
+
const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
|
|
103
|
+
const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
|
|
80
104
|
const mappedType = this.#platform.getMappedType(type);
|
|
81
105
|
if (mappedType instanceof DecimalType) {
|
|
82
106
|
const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
|
|
@@ -113,6 +137,7 @@ export class DatabaseTable {
|
|
|
113
137
|
default: prop.defaultRaw,
|
|
114
138
|
enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
|
|
115
139
|
comment: prop.comment,
|
|
140
|
+
collation: prop.collation,
|
|
116
141
|
extra: prop.extra,
|
|
117
142
|
ignoreSchemaChanges: prop.ignoreSchemaChanges,
|
|
118
143
|
};
|
|
@@ -185,6 +210,7 @@ export class DatabaseTable {
|
|
|
185
210
|
const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames } = this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
|
|
186
211
|
const name = namingStrategy.getEntityName(this.name, this.schema);
|
|
187
212
|
const schema = new EntitySchema({ name, collection: this.name, schema: this.schema, comment: this.comment });
|
|
213
|
+
schema.meta.partitionBy = toEntityPartitionBy(this.partitioning, this.name, this.schema);
|
|
188
214
|
const compositeFkIndexes = {};
|
|
189
215
|
const compositeFkUniques = {};
|
|
190
216
|
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.
|
|
@@ -204,6 +230,7 @@ export class DatabaseTable {
|
|
|
204
230
|
name: index.keyName,
|
|
205
231
|
deferMode: index.deferMode,
|
|
206
232
|
expression: index.expression,
|
|
233
|
+
where: index.where,
|
|
207
234
|
// Advanced index options - convert column names to property names
|
|
208
235
|
columns: index.columns?.map(col => ({
|
|
209
236
|
...col,
|
|
@@ -234,7 +261,7 @@ export class DatabaseTable {
|
|
|
234
261
|
index.invisible ||
|
|
235
262
|
index.disabled ||
|
|
236
263
|
index.clustered;
|
|
237
|
-
const isTrivial = !index.deferMode && !index.expression && !hasAdvancedOptions;
|
|
264
|
+
const isTrivial = !index.deferMode && !index.expression && !index.where && !hasAdvancedOptions;
|
|
238
265
|
if (isTrivial) {
|
|
239
266
|
// Index is for FK. Map to the FK prop and move on.
|
|
240
267
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -646,6 +673,7 @@ export class DatabaseTable {
|
|
|
646
673
|
columnOptions.scale = column.scale;
|
|
647
674
|
columnOptions.extra = column.extra;
|
|
648
675
|
columnOptions.comment = column.comment;
|
|
676
|
+
columnOptions.collation = column.collation;
|
|
649
677
|
columnOptions.enum = !!column.enumItems?.length;
|
|
650
678
|
columnOptions.items = column.enumItems;
|
|
651
679
|
}
|
|
@@ -712,6 +740,7 @@ export class DatabaseTable {
|
|
|
712
740
|
scale: column.scale,
|
|
713
741
|
extra: column.extra,
|
|
714
742
|
comment: column.comment,
|
|
743
|
+
collation: column.collation,
|
|
715
744
|
index: index ? index.keyName : undefined,
|
|
716
745
|
unique: unique ? unique.keyName : undefined,
|
|
717
746
|
enum: !!column.enumItems?.length,
|
|
@@ -876,16 +905,25 @@ export class DatabaseTable {
|
|
|
876
905
|
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
|
|
877
906
|
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`);
|
|
878
907
|
}
|
|
908
|
+
// The `expression` escape hatch takes the full index definition as raw SQL; combining it
|
|
909
|
+
// with `where` is dialect-dependent (PG/Oracle/MySQL drop `where`, MSSQL appends it) so we
|
|
910
|
+
// reject the combination up-front and ask users to inline the predicate into `expression`.
|
|
911
|
+
if (index.expression && index.where != null) {
|
|
912
|
+
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\`.`);
|
|
913
|
+
}
|
|
914
|
+
const where = this.processIndexWhere(index.where, meta);
|
|
879
915
|
this.#indexes.push({
|
|
880
916
|
keyName: name,
|
|
881
917
|
columnNames: properties,
|
|
882
918
|
composite: properties.length > 1,
|
|
883
|
-
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
884
|
-
|
|
919
|
+
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them.
|
|
920
|
+
// Partial indexes (`where`) must use CREATE [UNIQUE] INDEX form — constraints can't carry predicates.
|
|
921
|
+
constraint: type !== 'index' && !properties.some((d) => d.includes('.')) && !where,
|
|
885
922
|
primary: type === 'primary',
|
|
886
923
|
unique: type !== 'index',
|
|
887
924
|
type: index.type,
|
|
888
925
|
expression: this.processIndexExpression(name, index.expression, meta),
|
|
926
|
+
where,
|
|
889
927
|
options: index.options,
|
|
890
928
|
deferMode: index.deferMode,
|
|
891
929
|
columns,
|
|
@@ -896,6 +934,15 @@ export class DatabaseTable {
|
|
|
896
934
|
clustered: index.clustered,
|
|
897
935
|
});
|
|
898
936
|
}
|
|
937
|
+
processIndexWhere(where, meta) {
|
|
938
|
+
if (where == null) {
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
// The driver is always an `AbstractSqlDriver` here — `DatabaseTable` is only instantiated
|
|
942
|
+
// by SQL-side schema code, so the platform's config-bound driver is guaranteed to be one.
|
|
943
|
+
const driver = this.#platform.getConfig().getDriver();
|
|
944
|
+
return driver.renderPartialIndexWhere(meta.class, where);
|
|
945
|
+
}
|
|
899
946
|
addCheck(check) {
|
|
900
947
|
this.#checks.push(check);
|
|
901
948
|
}
|
|
@@ -904,52 +951,138 @@ export class DatabaseTable {
|
|
|
904
951
|
}
|
|
905
952
|
toJSON() {
|
|
906
953
|
const columns = this.#columns;
|
|
907
|
-
|
|
954
|
+
// locale-independent comparison so the snapshot is stable across machines
|
|
955
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
956
|
+
const sortedColumnKeys = Utils.keys(columns).sort(byString);
|
|
957
|
+
// mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
|
|
958
|
+
// so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
|
|
959
|
+
const primaryColumns = new Set();
|
|
960
|
+
const uniqueColumns = new Set();
|
|
961
|
+
for (const idx of this.#indexes) {
|
|
962
|
+
if (idx.primary) {
|
|
963
|
+
idx.columnNames.forEach(c => primaryColumns.add(c));
|
|
964
|
+
}
|
|
965
|
+
if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
|
|
966
|
+
uniqueColumns.add(idx.columnNames[0]);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
|
|
970
|
+
const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
|
|
971
|
+
mappedType instanceof t.smallint ||
|
|
972
|
+
mappedType instanceof t.tinyint ||
|
|
973
|
+
mappedType instanceof t.mediumint ||
|
|
974
|
+
mappedType instanceof t.bigint ||
|
|
975
|
+
mappedType instanceof t.float ||
|
|
976
|
+
mappedType instanceof t.double;
|
|
977
|
+
const supportsUnsigned = this.#platform.supportsUnsigned();
|
|
978
|
+
const columnsMapped = sortedColumnKeys.reduce((o, col) => {
|
|
908
979
|
const c = columns[col];
|
|
980
|
+
// omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
|
|
981
|
+
const rawType = c.type?.toLowerCase();
|
|
982
|
+
const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
|
|
983
|
+
const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
|
|
984
|
+
const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
|
|
909
985
|
const normalized = {
|
|
910
986
|
name: c.name,
|
|
911
|
-
type
|
|
912
|
-
unsigned: !!c.unsigned,
|
|
987
|
+
type,
|
|
988
|
+
unsigned: supportsUnsigned && !!c.unsigned,
|
|
913
989
|
autoincrement: !!c.autoincrement,
|
|
914
|
-
primary: !!c.primary,
|
|
990
|
+
primary: primaryColumns.has(c.name) || !!c.primary,
|
|
915
991
|
nullable: !!c.nullable,
|
|
916
|
-
unique: !!c.unique,
|
|
917
|
-
length: c.length
|
|
918
|
-
precision: c.precision ?? null,
|
|
919
|
-
scale: c.scale ?? null,
|
|
992
|
+
unique: uniqueColumns.has(c.name) || !!c.unique,
|
|
993
|
+
length: c.length || null,
|
|
994
|
+
precision: fixedPrecision ? null : (c.precision ?? null),
|
|
995
|
+
scale: fixedPrecision ? null : (c.scale ?? null),
|
|
920
996
|
default: c.default ?? null,
|
|
921
997
|
comment: c.comment ?? null,
|
|
998
|
+
collation: c.collation ?? null,
|
|
922
999
|
enumItems: c.enumItems ?? [],
|
|
923
1000
|
mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
|
|
924
1001
|
};
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
|
|
936
|
-
}
|
|
937
|
-
if (c.defaultConstraint) {
|
|
938
|
-
normalized.defaultConstraint = c.defaultConstraint;
|
|
1002
|
+
for (const field of [
|
|
1003
|
+
'generated',
|
|
1004
|
+
'nativeEnumName',
|
|
1005
|
+
'extra',
|
|
1006
|
+
'ignoreSchemaChanges',
|
|
1007
|
+
'defaultConstraint',
|
|
1008
|
+
]) {
|
|
1009
|
+
if (c[field]) {
|
|
1010
|
+
normalized[field] = c[field];
|
|
1011
|
+
}
|
|
939
1012
|
}
|
|
940
1013
|
o[col] = normalized;
|
|
941
1014
|
return o;
|
|
942
1015
|
}, {});
|
|
1016
|
+
const normalizeIndex = (idx) => {
|
|
1017
|
+
const out = {
|
|
1018
|
+
columnNames: idx.columnNames,
|
|
1019
|
+
composite: !!idx.composite,
|
|
1020
|
+
// PK indexes are always backed by a constraint — force it so postgres introspection matches
|
|
1021
|
+
constraint: !!idx.constraint || !!idx.primary,
|
|
1022
|
+
keyName: idx.keyName,
|
|
1023
|
+
primary: !!idx.primary,
|
|
1024
|
+
unique: !!idx.unique,
|
|
1025
|
+
};
|
|
1026
|
+
const optional = [
|
|
1027
|
+
'expression',
|
|
1028
|
+
'type',
|
|
1029
|
+
'deferMode',
|
|
1030
|
+
'columns',
|
|
1031
|
+
'include',
|
|
1032
|
+
'fillFactor',
|
|
1033
|
+
'invisible',
|
|
1034
|
+
'disabled',
|
|
1035
|
+
'clustered',
|
|
1036
|
+
];
|
|
1037
|
+
for (const field of optional) {
|
|
1038
|
+
if (idx[field] != null && idx[field] !== false) {
|
|
1039
|
+
out[field] = idx[field];
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
};
|
|
1044
|
+
const normalizeFk = (fk) => {
|
|
1045
|
+
const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
|
|
1046
|
+
// JSON.stringify drops undefined properties — let them through instead of guarding
|
|
1047
|
+
return {
|
|
1048
|
+
columnNames: fk.columnNames,
|
|
1049
|
+
constraintName: fk.constraintName,
|
|
1050
|
+
localTableName: fk.localTableName,
|
|
1051
|
+
referencedColumnNames: fk.referencedColumnNames,
|
|
1052
|
+
referencedTableName: fk.referencedTableName,
|
|
1053
|
+
updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
|
|
1054
|
+
deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
|
|
1055
|
+
deferMode: fk.deferMode,
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
const normalizeCheck = (check) => {
|
|
1059
|
+
const out = { name: check.name };
|
|
1060
|
+
if (typeof check.expression === 'string') {
|
|
1061
|
+
out.expression = check.expression;
|
|
1062
|
+
}
|
|
1063
|
+
for (const field of ['definition', 'columnName']) {
|
|
1064
|
+
if (check[field]) {
|
|
1065
|
+
out[field] = check[field];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
};
|
|
1070
|
+
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1071
|
+
const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
|
|
1072
|
+
const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
|
|
1073
|
+
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1074
|
+
.sort(([a], [b]) => byString(a, b))
|
|
1075
|
+
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
943
1076
|
return {
|
|
944
1077
|
name: this.name,
|
|
945
1078
|
schema: this.schema,
|
|
946
1079
|
columns: columnsMapped,
|
|
947
|
-
indexes:
|
|
948
|
-
checks:
|
|
949
|
-
triggers:
|
|
950
|
-
foreignKeys:
|
|
951
|
-
|
|
952
|
-
comment: this.comment,
|
|
1080
|
+
indexes: sortedIndexes,
|
|
1081
|
+
checks: sortedChecks,
|
|
1082
|
+
triggers: sortedTriggers,
|
|
1083
|
+
foreignKeys: sortedForeignKeys,
|
|
1084
|
+
// emit `comment` even when unset so introspection (which always reads it) matches metadata
|
|
1085
|
+
comment: this.comment ?? null,
|
|
953
1086
|
};
|
|
954
1087
|
}
|
|
955
1088
|
}
|
|
@@ -17,6 +17,15 @@ export declare class SchemaComparator {
|
|
|
17
17
|
* stored in toSchema.
|
|
18
18
|
*/
|
|
19
19
|
compare(fromSchema: DatabaseSchema, toSchema: DatabaseSchema, inverseDiff?: SchemaDifference): SchemaDifference;
|
|
20
|
+
private compareRoutines;
|
|
21
|
+
private diffRoutine;
|
|
22
|
+
/** Strips outer `BEGIN ... END` and trailing semicolons so the comparator doesn't churn on cosmetic round-trip differences. */
|
|
23
|
+
private normaliseBody;
|
|
24
|
+
private diffRoutineParams;
|
|
25
|
+
/** Engines drop length/precision modifiers and use different aliases for the same logical type — canonicalise both sides. */
|
|
26
|
+
private normaliseParamType;
|
|
27
|
+
private static readonly PARAM_TYPE_ALIASES;
|
|
28
|
+
private diffRoutineReturns;
|
|
20
29
|
/**
|
|
21
30
|
* Returns the difference between the tables fromTable and toTable.
|
|
22
31
|
* If there are no differences this method returns the boolean false.
|
|
@@ -39,6 +48,15 @@ export declare class SchemaComparator {
|
|
|
39
48
|
diffColumn(fromColumn: Column, toColumn: Column, fromTable: DatabaseTable, logging?: boolean): Set<string>;
|
|
40
49
|
diffEnumItems(items1?: string[], items2?: string[]): boolean;
|
|
41
50
|
diffComment(comment1?: string, comment2?: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* `from` is the introspected DB state, `to` is the target metadata. A column-level `COLLATE`
|
|
53
|
+
* clause naming the table/database default is just verbose syntax for inheriting that default,
|
|
54
|
+
* so both sides are normalized — anything matching `tableDefault` collapses to `undefined` and
|
|
55
|
+
* compares equal to "no explicit collation". Comparison is case-insensitive on dialects that
|
|
56
|
+
* treat collation identifiers as case-insensitive (MySQL/MSSQL/SQLite); PostgreSQL's
|
|
57
|
+
* `pg_collation.collname` is case-sensitive and is compared verbatim.
|
|
58
|
+
*/
|
|
59
|
+
diffCollation(fromCollation?: string, toCollation?: string, tableDefault?: string): boolean;
|
|
42
60
|
/**
|
|
43
61
|
* Finds the difference between the indexes index1 and index2.
|
|
44
62
|
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
|
@@ -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
|
*/
|
|
@@ -27,6 +28,9 @@ export class SchemaComparator {
|
|
|
27
28
|
newViews: {},
|
|
28
29
|
changedViews: {},
|
|
29
30
|
removedViews: {},
|
|
31
|
+
newRoutines: {},
|
|
32
|
+
changedRoutines: {},
|
|
33
|
+
removedRoutines: {},
|
|
30
34
|
orphanedForeignKeys: [],
|
|
31
35
|
newNativeEnums: [],
|
|
32
36
|
removedNativeEnums: [],
|
|
@@ -162,8 +166,151 @@ export class SchemaComparator {
|
|
|
162
166
|
diff.changedTables[viewName] = tableDiff;
|
|
163
167
|
}
|
|
164
168
|
}
|
|
169
|
+
this.compareRoutines(fromSchema, toSchema, diff);
|
|
165
170
|
return diff;
|
|
166
171
|
}
|
|
172
|
+
compareRoutines(fromSchema, toSchema, diff) {
|
|
173
|
+
// Case-fold so user-written `'sql_hash'` matches Oracle's introspected `'SQL_HASH'`.
|
|
174
|
+
const routineKey = (r) => ((r.schema ? `${r.schema}.` : '') + r.name).toLowerCase();
|
|
175
|
+
const fromByKey = new Map(fromSchema.getRoutines().map(r => [routineKey(r), r]));
|
|
176
|
+
const toByKey = new Map(toSchema.getRoutines().map(r => [routineKey(r), r]));
|
|
177
|
+
for (const [key, toRoutine] of toByKey) {
|
|
178
|
+
const fromRoutine = fromByKey.get(key);
|
|
179
|
+
if (!fromRoutine) {
|
|
180
|
+
diff.newRoutines[key] = toRoutine;
|
|
181
|
+
this.log(`routine ${key} added`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (this.diffRoutine(fromRoutine, toRoutine)) {
|
|
185
|
+
diff.changedRoutines[key] = { from: fromRoutine, to: toRoutine };
|
|
186
|
+
this.log(`routine ${key} changed`, { fromRoutine, toRoutine });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const [key, fromRoutine] of fromByKey) {
|
|
190
|
+
if (!toByKey.has(key)) {
|
|
191
|
+
diff.removedRoutines[key] = fromRoutine;
|
|
192
|
+
this.log(`routine ${key} removed`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
diffRoutine(from, to) {
|
|
197
|
+
const ignore = new Set(to.ignoreSchemaChanges ?? []);
|
|
198
|
+
if (from.type !== to.type) {
|
|
199
|
+
this.log(`routine ${from.name}: type ${from.type} -> ${to.type}`);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (!ignore.has('body') && from.expression == null && to.expression == null) {
|
|
203
|
+
const a = this.normaliseBody(from.body);
|
|
204
|
+
const b = this.normaliseBody(to.body);
|
|
205
|
+
if (a !== b) {
|
|
206
|
+
this.log(`routine ${from.name}: body differs`, { from: a, to: b });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!ignore.has('comment') && (from.comment ?? '') !== (to.comment ?? '')) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
// For security/deterministic/definer, unset on the metadata side means "don't care": the
|
|
214
|
+
// DB always has some server default, comparing it would force a drop+create on every run.
|
|
215
|
+
if (!ignore.has('security') && to.security != null && from.security !== to.security) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (!ignore.has('deterministic') &&
|
|
219
|
+
to.deterministic != null &&
|
|
220
|
+
(from.deterministic ?? false) !== to.deterministic) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (!ignore.has('definer') && to.definer != null && from.definer !== to.definer) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
// `language` (PG) / `dataAccess` (MySQL) follow the same to-side-wins policy: only diff when
|
|
227
|
+
// the metadata explicitly declares them, otherwise the create DDL's defaults will line up with
|
|
228
|
+
// whatever the engine introspected.
|
|
229
|
+
if (to.language != null && (from.language ?? '').toLowerCase() !== to.language.toLowerCase()) {
|
|
230
|
+
this.log(`routine ${from.name}: language ${from.language} -> ${to.language}`);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (to.dataAccess != null && from.dataAccess !== to.dataAccess) {
|
|
234
|
+
this.log(`routine ${from.name}: dataAccess ${from.dataAccess} -> ${to.dataAccess}`);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (this.diffRoutineParams(from.params, to.params)) {
|
|
238
|
+
this.log(`routine ${from.name}: params differ`, { from: from.params, to: to.params });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
if (this.diffRoutineReturns(from.returns, to.returns)) {
|
|
242
|
+
this.log(`routine ${from.name}: returns differ`, { from: from.returns, to: to.returns });
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
/** Strips outer `BEGIN ... END` and trailing semicolons so the comparator doesn't churn on cosmetic round-trip differences. */
|
|
248
|
+
normaliseBody(body) {
|
|
249
|
+
let result = (body ?? '').replace(/\s+/g, ' ').trim();
|
|
250
|
+
const beginEnd = /^begin\s+([\s\S]*?)\s*end\s*;?\s*$/i.exec(result);
|
|
251
|
+
if (beginEnd) {
|
|
252
|
+
result = beginEnd[1].trim();
|
|
253
|
+
}
|
|
254
|
+
return result.replace(/;+\s*$/, '').trim();
|
|
255
|
+
}
|
|
256
|
+
diffRoutineParams(from, to) {
|
|
257
|
+
if (from.length !== to.length) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
for (let i = 0; i < from.length; i++) {
|
|
261
|
+
const a = from[i];
|
|
262
|
+
const b = to[i];
|
|
263
|
+
if (a.name.toLowerCase() !== b.name.toLowerCase() ||
|
|
264
|
+
this.normaliseParamType(a.type) !== this.normaliseParamType(b.type) ||
|
|
265
|
+
a.direction !== b.direction) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
// Asymmetric: drivers don't currently introspect param nullability, so the from side is
|
|
269
|
+
// always undefined; comparing eagerly would churn metadata-declared `nullable: true`.
|
|
270
|
+
if (a.nullable != null && !!a.nullable !== !!b.nullable) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
/** Engines drop length/precision modifiers and use different aliases for the same logical type — canonicalise both sides. */
|
|
277
|
+
normaliseParamType(type) {
|
|
278
|
+
const lengthMatch = /^([^()]+)\(([^)]*)\)\s*$/.exec(type);
|
|
279
|
+
const base = (lengthMatch ? lengthMatch[1] : type).trim().toLowerCase();
|
|
280
|
+
const aliased = SchemaComparator.PARAM_TYPE_ALIASES[base] ?? base;
|
|
281
|
+
const length = lengthMatch ? Number.parseInt(lengthMatch[2].split(',')[0].trim(), 10) : NaN;
|
|
282
|
+
const options = Number.isFinite(length) ? { length } : {};
|
|
283
|
+
return this.#platform.normalizeColumnType(aliased, options).toLowerCase();
|
|
284
|
+
}
|
|
285
|
+
static PARAM_TYPE_ALIASES = {
|
|
286
|
+
int: 'integer',
|
|
287
|
+
int4: 'integer',
|
|
288
|
+
int8: 'bigint',
|
|
289
|
+
int2: 'smallint',
|
|
290
|
+
bool: 'boolean',
|
|
291
|
+
'character varying': 'varchar',
|
|
292
|
+
bpchar: 'char',
|
|
293
|
+
float8: 'double precision',
|
|
294
|
+
float4: 'real',
|
|
295
|
+
// Oracle's USER_ARGUMENTS reports `REF CURSOR`; users declare `sys_refcursor`.
|
|
296
|
+
'ref cursor': 'sys_refcursor',
|
|
297
|
+
};
|
|
298
|
+
diffRoutineReturns(from, to) {
|
|
299
|
+
if (from == null && to == null) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (from == null || to == null) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (this.normaliseParamType(from.type) !== this.normaliseParamType(to.type)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
// Engines report function returns as nullable; only diff when metadata explicitly declares it.
|
|
309
|
+
if (to.nullable != null && (from.nullable ?? false) !== to.nullable) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
167
314
|
/**
|
|
168
315
|
* Returns the difference between the tables fromTable and toTable.
|
|
169
316
|
* If there are no differences this method returns the boolean false.
|
|
@@ -200,6 +347,17 @@ export class SchemaComparator {
|
|
|
200
347
|
});
|
|
201
348
|
changes++;
|
|
202
349
|
}
|
|
350
|
+
if (diffPartitioning(fromTable.getPartitioning(), toTable.getPartitioning(), this.#platform.getDefaultSchemaName())) {
|
|
351
|
+
tableDifferences.changedPartitioning = {
|
|
352
|
+
from: fromTable.getPartitioning(),
|
|
353
|
+
to: toTable.getPartitioning(),
|
|
354
|
+
};
|
|
355
|
+
this.log(`table partitioning changed for ${tableDifferences.name}`, {
|
|
356
|
+
fromPartitioning: fromTable.getPartitioning(),
|
|
357
|
+
toPartitioning: toTable.getPartitioning(),
|
|
358
|
+
});
|
|
359
|
+
changes++;
|
|
360
|
+
}
|
|
203
361
|
const fromTableColumns = fromTable.getColumns();
|
|
204
362
|
const toTableColumns = toTable.getColumns();
|
|
205
363
|
// See if all the columns in "from" table exist in "to" table
|
|
@@ -266,6 +424,19 @@ export class SchemaComparator {
|
|
|
266
424
|
if (!this.diffIndex(index, toTableIndex)) {
|
|
267
425
|
continue;
|
|
268
426
|
}
|
|
427
|
+
// Constraint-vs-index form mismatch needs drop+create with the OLD form's drop SQL,
|
|
428
|
+
// which `changedIndexes` (uses new form only) can't do. Primary keys stay on the
|
|
429
|
+
// changed path which emits `add primary key`.
|
|
430
|
+
if (!index.primary && !!index.constraint !== !!toTableIndex.constraint) {
|
|
431
|
+
tableDifferences.removedIndexes[index.keyName] = index;
|
|
432
|
+
tableDifferences.addedIndexes[index.keyName] = toTableIndex;
|
|
433
|
+
this.log(`index ${index.keyName} changed form in table ${tableDifferences.name}`, {
|
|
434
|
+
fromTableIndex: index,
|
|
435
|
+
toTableIndex,
|
|
436
|
+
});
|
|
437
|
+
changes += 2;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
269
440
|
tableDifferences.changedIndexes[index.keyName] = toTableIndex;
|
|
270
441
|
this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
|
|
271
442
|
fromTableIndex: index,
|
|
@@ -546,6 +717,11 @@ export class SchemaComparator {
|
|
|
546
717
|
log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
547
718
|
changedProperties.add('comment');
|
|
548
719
|
}
|
|
720
|
+
if (!(fromColumn.ignoreSchemaChanges?.includes('collation') || toColumn.ignoreSchemaChanges?.includes('collation')) &&
|
|
721
|
+
this.diffCollation(fromColumn.collation, toColumn.collation, fromTable.collation)) {
|
|
722
|
+
log(`'collation' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
723
|
+
changedProperties.add('collation');
|
|
724
|
+
}
|
|
549
725
|
const isNonNativeEnumArray = !(fromColumn.nativeEnumName || toColumn.nativeEnumName) &&
|
|
550
726
|
(fromColumn.mappedType instanceof ArrayType || toColumn.mappedType instanceof ArrayType);
|
|
551
727
|
if (!isNonNativeEnumArray && this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
|
|
@@ -567,12 +743,26 @@ export class SchemaComparator {
|
|
|
567
743
|
// eslint-disable-next-line eqeqeq
|
|
568
744
|
return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
|
|
569
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* `from` is the introspected DB state, `to` is the target metadata. A column-level `COLLATE`
|
|
748
|
+
* clause naming the table/database default is just verbose syntax for inheriting that default,
|
|
749
|
+
* so both sides are normalized — anything matching `tableDefault` collapses to `undefined` and
|
|
750
|
+
* compares equal to "no explicit collation". Comparison is case-insensitive on dialects that
|
|
751
|
+
* treat collation identifiers as case-insensitive (MySQL/MSSQL/SQLite); PostgreSQL's
|
|
752
|
+
* `pg_collation.collname` is case-sensitive and is compared verbatim.
|
|
753
|
+
*/
|
|
754
|
+
diffCollation(fromCollation, toCollation, tableDefault) {
|
|
755
|
+
const fold = this.#platform.caseInsensitiveCollationNames() ? (s) => s.toLowerCase() : (s) => s;
|
|
756
|
+
const norm = (c) => c && tableDefault && fold(c) === fold(tableDefault) ? undefined : c == null ? undefined : fold(c);
|
|
757
|
+
return norm(fromCollation) !== norm(toCollation);
|
|
758
|
+
}
|
|
570
759
|
/**
|
|
571
760
|
* Finds the difference between the indexes index1 and index2.
|
|
572
761
|
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
|
573
762
|
*/
|
|
574
763
|
diffIndex(index1, index2) {
|
|
575
|
-
//
|
|
764
|
+
// Opaque raw expressions (`expression` escape hatch) and full-text indexes can't be
|
|
765
|
+
// compared structurally — fall back to name-only matching.
|
|
576
766
|
if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
|
|
577
767
|
return index1.keyName !== index2.keyName;
|
|
578
768
|
}
|
|
@@ -623,6 +813,11 @@ export class SchemaComparator {
|
|
|
623
813
|
if (!!index1.clustered !== !!index2.clustered) {
|
|
624
814
|
return false;
|
|
625
815
|
}
|
|
816
|
+
// Compare WHERE predicate of partial indexes structurally (whitespace/quoting/casing
|
|
817
|
+
// are normalized via the same helper used for check constraints).
|
|
818
|
+
if (this.diffExpression(index1.where ?? '', index2.where ?? '')) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
626
821
|
if (!index1.unique && !index1.primary) {
|
|
627
822
|
// this is a special case: If the current key is neither primary or unique, any unique or
|
|
628
823
|
// primary key will always have the same effect for the index and there cannot be any constraint
|