@mikro-orm/sql 7.0.17-dev.9 → 7.0.18-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +27 -6
- package/AbstractSqlDriver.d.ts +25 -1
- package/AbstractSqlDriver.js +356 -20
- package/AbstractSqlPlatform.d.ts +13 -2
- package/AbstractSqlPlatform.js +16 -3
- package/PivotCollectionPersister.d.ts +2 -2
- package/PivotCollectionPersister.js +19 -3
- package/README.md +2 -1
- package/SqlEntityManager.d.ts +46 -3
- package/SqlEntityManager.js +77 -7
- package/SqlMikroORM.d.ts +4 -4
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +19 -3
- package/dialects/mysql/MySqlSchemaHelper.js +254 -21
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
- package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +8 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +50 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +38 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +341 -6
- package/dialects/postgresql/index.d.ts +2 -0
- package/dialects/postgresql/index.js +2 -0
- package/dialects/postgresql/typeOverrides.d.ts +14 -0
- package/dialects/postgresql/typeOverrides.js +12 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +7 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +131 -2
- package/package.json +4 -4
- package/query/NativeQueryBuilder.d.ts +6 -0
- package/query/NativeQueryBuilder.js +16 -1
- package/query/QueryBuilder.d.ts +83 -1
- package/query/QueryBuilder.js +181 -8
- package/schema/DatabaseSchema.d.ts +29 -2
- package/schema/DatabaseSchema.js +137 -0
- package/schema/DatabaseTable.d.ts +20 -1
- package/schema/DatabaseTable.js +62 -3
- package/schema/SchemaComparator.d.ts +19 -0
- package/schema/SchemaComparator.js +250 -1
- package/schema/SchemaHelper.d.ts +77 -1
- package/schema/SchemaHelper.js +279 -5
- package/schema/SqlSchemaGenerator.d.ts +2 -2
- package/schema/SqlSchemaGenerator.js +47 -10
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +69 -2
package/schema/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,19 @@ 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;
|
|
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;
|
|
15
25
|
constructor(platform, name, schema) {
|
|
16
26
|
this.name = name;
|
|
17
27
|
this.schema = schema;
|
|
@@ -35,6 +45,16 @@ export class DatabaseTable {
|
|
|
35
45
|
getChecks() {
|
|
36
46
|
return this.#checks;
|
|
37
47
|
}
|
|
48
|
+
getPartitioning() {
|
|
49
|
+
return this.partitioning;
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
setPartitioning(partitioning) {
|
|
53
|
+
this.partitioning = partitioning;
|
|
54
|
+
}
|
|
55
|
+
getTriggers() {
|
|
56
|
+
return this.#triggers;
|
|
57
|
+
}
|
|
38
58
|
/** @internal */
|
|
39
59
|
setIndexes(indexes) {
|
|
40
60
|
this.#indexes = indexes;
|
|
@@ -44,6 +64,10 @@ export class DatabaseTable {
|
|
|
44
64
|
this.#checks = checks;
|
|
45
65
|
}
|
|
46
66
|
/** @internal */
|
|
67
|
+
setTriggers(triggers) {
|
|
68
|
+
this.#triggers = triggers;
|
|
69
|
+
}
|
|
70
|
+
/** @internal */
|
|
47
71
|
setForeignKeys(fks) {
|
|
48
72
|
this.#foreignKeys = fks;
|
|
49
73
|
}
|
|
@@ -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);
|
|
@@ -606,6 +633,12 @@ export class DatabaseTable {
|
|
|
606
633
|
hasCheck(checkName) {
|
|
607
634
|
return !!this.getCheck(checkName);
|
|
608
635
|
}
|
|
636
|
+
getTrigger(triggerName) {
|
|
637
|
+
return this.#triggers.find(t => t.name === triggerName);
|
|
638
|
+
}
|
|
639
|
+
hasTrigger(triggerName) {
|
|
640
|
+
return !!this.getTrigger(triggerName);
|
|
641
|
+
}
|
|
609
642
|
getPrimaryKey() {
|
|
610
643
|
return this.#indexes.find(i => i.primary);
|
|
611
644
|
}
|
|
@@ -640,6 +673,7 @@ export class DatabaseTable {
|
|
|
640
673
|
columnOptions.scale = column.scale;
|
|
641
674
|
columnOptions.extra = column.extra;
|
|
642
675
|
columnOptions.comment = column.comment;
|
|
676
|
+
columnOptions.collation = column.collation;
|
|
643
677
|
columnOptions.enum = !!column.enumItems?.length;
|
|
644
678
|
columnOptions.items = column.enumItems;
|
|
645
679
|
}
|
|
@@ -706,6 +740,7 @@ export class DatabaseTable {
|
|
|
706
740
|
scale: column.scale,
|
|
707
741
|
extra: column.extra,
|
|
708
742
|
comment: column.comment,
|
|
743
|
+
collation: column.collation,
|
|
709
744
|
index: index ? index.keyName : undefined,
|
|
710
745
|
unique: unique ? unique.keyName : undefined,
|
|
711
746
|
enum: !!column.enumItems?.length,
|
|
@@ -870,16 +905,25 @@ export class DatabaseTable {
|
|
|
870
905
|
if (index.fillFactor != null && (index.fillFactor < 0 || index.fillFactor > 100)) {
|
|
871
906
|
throw new Error(`fillFactor must be between 0 and 100, got ${index.fillFactor} for index '${name}' on entity '${meta.className}'`);
|
|
872
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);
|
|
873
915
|
this.#indexes.push({
|
|
874
916
|
keyName: name,
|
|
875
917
|
columnNames: properties,
|
|
876
918
|
composite: properties.length > 1,
|
|
877
|
-
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
878
|
-
|
|
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,
|
|
879
922
|
primary: type === 'primary',
|
|
880
923
|
unique: type !== 'index',
|
|
881
924
|
type: index.type,
|
|
882
925
|
expression: this.processIndexExpression(name, index.expression, meta),
|
|
926
|
+
where,
|
|
883
927
|
options: index.options,
|
|
884
928
|
deferMode: index.deferMode,
|
|
885
929
|
columns,
|
|
@@ -890,9 +934,21 @@ export class DatabaseTable {
|
|
|
890
934
|
clustered: index.clustered,
|
|
891
935
|
});
|
|
892
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
|
+
}
|
|
893
946
|
addCheck(check) {
|
|
894
947
|
this.#checks.push(check);
|
|
895
948
|
}
|
|
949
|
+
addTrigger(trigger) {
|
|
950
|
+
this.#triggers.push(trigger);
|
|
951
|
+
}
|
|
896
952
|
toJSON() {
|
|
897
953
|
const columns = this.#columns;
|
|
898
954
|
// locale-independent comparison so the snapshot is stable across machines
|
|
@@ -939,6 +995,7 @@ export class DatabaseTable {
|
|
|
939
995
|
scale: fixedPrecision ? null : (c.scale ?? null),
|
|
940
996
|
default: c.default ?? null,
|
|
941
997
|
comment: c.comment ?? null,
|
|
998
|
+
collation: c.collation ?? null,
|
|
942
999
|
enumItems: c.enumItems ?? [],
|
|
943
1000
|
mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
|
|
944
1001
|
};
|
|
@@ -1012,6 +1069,7 @@ export class DatabaseTable {
|
|
|
1012
1069
|
};
|
|
1013
1070
|
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1014
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));
|
|
1015
1073
|
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1016
1074
|
.sort(([a], [b]) => byString(a, b))
|
|
1017
1075
|
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
@@ -1021,6 +1079,7 @@ export class DatabaseTable {
|
|
|
1021
1079
|
columns: columnsMapped,
|
|
1022
1080
|
indexes: sortedIndexes,
|
|
1023
1081
|
checks: sortedChecks,
|
|
1082
|
+
triggers: sortedTriggers,
|
|
1024
1083
|
foreignKeys: sortedForeignKeys,
|
|
1025
1084
|
// emit `comment` even when unset so introspection (which always reads it) matches metadata
|
|
1026
1085
|
comment: this.comment ?? null,
|
|
@@ -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.
|
|
@@ -69,6 +87,7 @@ export declare class SchemaComparator {
|
|
|
69
87
|
* @see https://github.com/mikro-orm/mikro-orm/issues/7308
|
|
70
88
|
*/
|
|
71
89
|
private diffViewExpression;
|
|
90
|
+
private diffTrigger;
|
|
72
91
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
73
92
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
74
93
|
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
|
*/
|
|
@@ -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.
|
|
@@ -176,14 +323,17 @@ export class SchemaComparator {
|
|
|
176
323
|
addedForeignKeys: {},
|
|
177
324
|
addedIndexes: {},
|
|
178
325
|
addedChecks: {},
|
|
326
|
+
addedTriggers: {},
|
|
179
327
|
changedColumns: {},
|
|
180
328
|
changedForeignKeys: {},
|
|
181
329
|
changedIndexes: {},
|
|
182
330
|
changedChecks: {},
|
|
331
|
+
changedTriggers: {},
|
|
183
332
|
removedColumns: {},
|
|
184
333
|
removedForeignKeys: {},
|
|
185
334
|
removedIndexes: {},
|
|
186
335
|
removedChecks: {},
|
|
336
|
+
removedTriggers: {},
|
|
187
337
|
renamedColumns: {},
|
|
188
338
|
renamedIndexes: {},
|
|
189
339
|
fromTable,
|
|
@@ -197,6 +347,17 @@ export class SchemaComparator {
|
|
|
197
347
|
});
|
|
198
348
|
changes++;
|
|
199
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
|
+
}
|
|
200
361
|
const fromTableColumns = fromTable.getColumns();
|
|
201
362
|
const toTableColumns = toTable.getColumns();
|
|
202
363
|
// See if all the columns in "from" table exist in "to" table
|
|
@@ -263,6 +424,19 @@ export class SchemaComparator {
|
|
|
263
424
|
if (!this.diffIndex(index, toTableIndex)) {
|
|
264
425
|
continue;
|
|
265
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
|
+
}
|
|
266
440
|
tableDifferences.changedIndexes[index.keyName] = toTableIndex;
|
|
267
441
|
this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
|
|
268
442
|
fromTableIndex: index,
|
|
@@ -309,6 +483,33 @@ export class SchemaComparator {
|
|
|
309
483
|
tableDifferences.changedChecks[check.name] = toTableCheck;
|
|
310
484
|
changes++;
|
|
311
485
|
}
|
|
486
|
+
const fromTableTriggers = fromTable.getTriggers();
|
|
487
|
+
const toTableTriggers = toTable.getTriggers();
|
|
488
|
+
for (const trigger of toTableTriggers) {
|
|
489
|
+
if (fromTable.hasTrigger(trigger.name)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
tableDifferences.addedTriggers[trigger.name] = trigger;
|
|
493
|
+
this.log(`trigger ${trigger.name} added to table ${tableDifferences.name}`, { trigger });
|
|
494
|
+
changes++;
|
|
495
|
+
}
|
|
496
|
+
for (const trigger of fromTableTriggers) {
|
|
497
|
+
if (!toTable.hasTrigger(trigger.name)) {
|
|
498
|
+
tableDifferences.removedTriggers[trigger.name] = trigger;
|
|
499
|
+
this.log(`trigger ${trigger.name} removed from table ${tableDifferences.name}`);
|
|
500
|
+
changes++;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const toTableTrigger = toTable.getTrigger(trigger.name);
|
|
504
|
+
if (this.diffTrigger(trigger, toTableTrigger)) {
|
|
505
|
+
this.log(`trigger ${trigger.name} changed in table ${tableDifferences.name}`, {
|
|
506
|
+
fromTableTrigger: trigger,
|
|
507
|
+
toTableTrigger,
|
|
508
|
+
});
|
|
509
|
+
tableDifferences.changedTriggers[trigger.name] = toTableTrigger;
|
|
510
|
+
changes++;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
312
513
|
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
|
313
514
|
const toForeignKeys = { ...toTable.getForeignKeys() };
|
|
314
515
|
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
|
@@ -516,6 +717,11 @@ export class SchemaComparator {
|
|
|
516
717
|
log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
517
718
|
changedProperties.add('comment');
|
|
518
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
|
+
}
|
|
519
725
|
const isNonNativeEnumArray = !(fromColumn.nativeEnumName || toColumn.nativeEnumName) &&
|
|
520
726
|
(fromColumn.mappedType instanceof ArrayType || toColumn.mappedType instanceof ArrayType);
|
|
521
727
|
if (!isNonNativeEnumArray && this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
|
|
@@ -537,12 +743,26 @@ export class SchemaComparator {
|
|
|
537
743
|
// eslint-disable-next-line eqeqeq
|
|
538
744
|
return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
|
|
539
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
|
+
}
|
|
540
759
|
/**
|
|
541
760
|
* Finds the difference between the indexes index1 and index2.
|
|
542
761
|
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
|
543
762
|
*/
|
|
544
763
|
diffIndex(index1, index2) {
|
|
545
|
-
//
|
|
764
|
+
// Opaque raw expressions (`expression` escape hatch) and full-text indexes can't be
|
|
765
|
+
// compared structurally — fall back to name-only matching.
|
|
546
766
|
if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
|
|
547
767
|
return index1.keyName !== index2.keyName;
|
|
548
768
|
}
|
|
@@ -593,6 +813,11 @@ export class SchemaComparator {
|
|
|
593
813
|
if (!!index1.clustered !== !!index2.clustered) {
|
|
594
814
|
return false;
|
|
595
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
|
+
}
|
|
596
821
|
if (!index1.unique && !index1.primary) {
|
|
597
822
|
// this is a special case: If the current key is neither primary or unique, any unique or
|
|
598
823
|
// primary key will always have the same effect for the index and there cannot be any constraint
|
|
@@ -714,6 +939,30 @@ export class SchemaComparator {
|
|
|
714
939
|
}
|
|
715
940
|
return true;
|
|
716
941
|
}
|
|
942
|
+
diffTrigger(from, to) {
|
|
943
|
+
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
944
|
+
// trigger metadata, so skip diffing when the metadata side uses it.
|
|
945
|
+
if (to.expression) {
|
|
946
|
+
// Both sides have expression — compare the raw DDL directly
|
|
947
|
+
if (from.expression) {
|
|
948
|
+
return this.diffExpression(from.expression, to.expression);
|
|
949
|
+
}
|
|
950
|
+
// Only metadata side has expression — the raw DDL cannot be compared to
|
|
951
|
+
// introspected metadata. Changes to the expression value won't be detected;
|
|
952
|
+
// drop and recreate the trigger manually to apply expression changes.
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
if (from.timing !== to.timing || from.forEach !== to.forEach) {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
if ([...from.events].sort().join(',') !== [...to.events].sort().join(',')) {
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
if ((from.when ?? '') !== (to.when ?? '')) {
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
return this.diffExpression(from.body, to.body);
|
|
965
|
+
}
|
|
717
966
|
parseJsonDefault(defaultValue) {
|
|
718
967
|
/* v8 ignore next */
|
|
719
968
|
if (!defaultValue) {
|