@mikro-orm/sql 7.0.9-dev.8 → 7.0.9

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.
Files changed (87) hide show
  1. package/AbstractSqlConnection.d.ts +94 -58
  2. package/AbstractSqlConnection.js +235 -238
  3. package/AbstractSqlDriver.d.ts +410 -155
  4. package/AbstractSqlDriver.js +2096 -1968
  5. package/AbstractSqlPlatform.d.ts +85 -75
  6. package/AbstractSqlPlatform.js +166 -162
  7. package/PivotCollectionPersister.d.ts +33 -15
  8. package/PivotCollectionPersister.js +158 -160
  9. package/README.md +1 -1
  10. package/SqlEntityManager.d.ts +67 -22
  11. package/SqlEntityManager.js +54 -38
  12. package/SqlEntityRepository.d.ts +14 -14
  13. package/SqlEntityRepository.js +23 -23
  14. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +12 -12
  15. package/dialects/mssql/MsSqlNativeQueryBuilder.js +199 -201
  16. package/dialects/mysql/BaseMySqlPlatform.d.ts +65 -46
  17. package/dialects/mysql/BaseMySqlPlatform.js +137 -134
  18. package/dialects/mysql/MySqlExceptionConverter.d.ts +6 -6
  19. package/dialects/mysql/MySqlExceptionConverter.js +91 -77
  20. package/dialects/mysql/MySqlNativeQueryBuilder.d.ts +3 -3
  21. package/dialects/mysql/MySqlNativeQueryBuilder.js +66 -69
  22. package/dialects/mysql/MySqlSchemaHelper.d.ts +58 -39
  23. package/dialects/mysql/MySqlSchemaHelper.js +327 -319
  24. package/dialects/oracledb/OracleDialect.d.ts +81 -52
  25. package/dialects/oracledb/OracleDialect.js +155 -149
  26. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +12 -12
  27. package/dialects/oracledb/OracleNativeQueryBuilder.js +239 -243
  28. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +109 -106
  29. package/dialects/postgresql/BasePostgreSqlPlatform.js +354 -353
  30. package/dialects/postgresql/FullTextType.d.ts +10 -6
  31. package/dialects/postgresql/FullTextType.js +51 -51
  32. package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +5 -5
  33. package/dialects/postgresql/PostgreSqlExceptionConverter.js +55 -43
  34. package/dialects/postgresql/PostgreSqlNativeQueryBuilder.d.ts +1 -1
  35. package/dialects/postgresql/PostgreSqlNativeQueryBuilder.js +4 -4
  36. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +117 -82
  37. package/dialects/postgresql/PostgreSqlSchemaHelper.js +747 -711
  38. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -5
  39. package/dialects/sqlite/BaseSqliteConnection.js +21 -19
  40. package/dialects/sqlite/NodeSqliteDialect.d.ts +1 -1
  41. package/dialects/sqlite/NodeSqliteDialect.js +23 -23
  42. package/dialects/sqlite/SqliteDriver.d.ts +1 -1
  43. package/dialects/sqlite/SqliteDriver.js +3 -3
  44. package/dialects/sqlite/SqliteExceptionConverter.d.ts +6 -6
  45. package/dialects/sqlite/SqliteExceptionConverter.js +67 -51
  46. package/dialects/sqlite/SqliteNativeQueryBuilder.d.ts +2 -2
  47. package/dialects/sqlite/SqliteNativeQueryBuilder.js +7 -7
  48. package/dialects/sqlite/SqlitePlatform.d.ts +63 -72
  49. package/dialects/sqlite/SqlitePlatform.js +139 -139
  50. package/dialects/sqlite/SqliteSchemaHelper.d.ts +77 -60
  51. package/dialects/sqlite/SqliteSchemaHelper.js +541 -522
  52. package/package.json +3 -3
  53. package/plugin/index.d.ts +42 -35
  54. package/plugin/index.js +43 -36
  55. package/plugin/transformer.d.ts +117 -94
  56. package/plugin/transformer.js +890 -881
  57. package/query/ArrayCriteriaNode.d.ts +4 -4
  58. package/query/ArrayCriteriaNode.js +18 -18
  59. package/query/CriteriaNode.d.ts +35 -25
  60. package/query/CriteriaNode.js +133 -123
  61. package/query/CriteriaNodeFactory.d.ts +49 -6
  62. package/query/CriteriaNodeFactory.js +97 -94
  63. package/query/NativeQueryBuilder.d.ts +120 -120
  64. package/query/NativeQueryBuilder.js +507 -501
  65. package/query/ObjectCriteriaNode.d.ts +12 -12
  66. package/query/ObjectCriteriaNode.js +298 -282
  67. package/query/QueryBuilder.d.ts +1557 -905
  68. package/query/QueryBuilder.js +2322 -2192
  69. package/query/QueryBuilderHelper.d.ts +153 -72
  70. package/query/QueryBuilderHelper.js +1080 -1028
  71. package/query/ScalarCriteriaNode.d.ts +3 -3
  72. package/query/ScalarCriteriaNode.js +53 -46
  73. package/query/enums.d.ts +14 -14
  74. package/query/enums.js +14 -14
  75. package/query/raw.d.ts +16 -6
  76. package/query/raw.js +10 -10
  77. package/schema/DatabaseSchema.d.ts +74 -50
  78. package/schema/DatabaseSchema.js +355 -327
  79. package/schema/DatabaseTable.d.ts +96 -73
  80. package/schema/DatabaseTable.js +1012 -927
  81. package/schema/SchemaComparator.d.ts +70 -66
  82. package/schema/SchemaComparator.js +790 -764
  83. package/schema/SchemaHelper.d.ts +121 -96
  84. package/schema/SchemaHelper.js +683 -668
  85. package/schema/SqlSchemaGenerator.d.ts +79 -59
  86. package/schema/SqlSchemaGenerator.js +525 -495
  87. package/typings.d.ts +405 -275
@@ -1,774 +1,800 @@
1
- import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils, } from '@mikro-orm/core';
1
+ import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils } from '@mikro-orm/core';
2
2
  import { DatabaseTable } from './DatabaseTable.js';
3
3
  /**
4
4
  * Compares two Schemas and return an instance of SchemaDifference.
5
5
  */
6
6
  export class SchemaComparator {
7
- #helper;
8
- #logger;
9
- #platform;
10
- constructor(platform) {
11
- this.#platform = platform;
12
- this.#helper = this.#platform.getSchemaHelper();
13
- this.#logger = this.#platform.getConfig().getLogger();
14
- }
15
- /**
16
- * Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema.
17
- *
18
- * The returned differences are returned in such a way that they contain the
19
- * operations to change the schema stored in fromSchema to the schema that is
20
- * stored in toSchema.
21
- */
22
- compare(fromSchema, toSchema, inverseDiff) {
23
- const diff = {
24
- newTables: {},
25
- removedTables: {},
26
- changedTables: {},
27
- newViews: {},
28
- changedViews: {},
29
- removedViews: {},
30
- orphanedForeignKeys: [],
31
- newNativeEnums: [],
32
- removedNativeEnums: [],
33
- newNamespaces: new Set(),
34
- removedNamespaces: new Set(),
35
- fromSchema,
36
- };
37
- const foreignKeysToTable = {};
38
- for (const namespace of toSchema.getNamespaces()) {
39
- if (fromSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
40
- continue;
41
- }
42
- diff.newNamespaces.add(namespace);
43
- }
44
- for (const namespace of fromSchema.getNamespaces()) {
45
- if (toSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
46
- continue;
47
- }
48
- diff.removedNamespaces.add(namespace);
49
- }
50
- for (const [key, nativeEnum] of Object.entries(toSchema.getNativeEnums())) {
51
- if (fromSchema.hasNativeEnum(key)) {
52
- continue;
53
- }
54
- if (nativeEnum.schema === '*' && fromSchema.hasNativeEnum(`${toSchema.name}.${key}`)) {
55
- continue;
56
- }
57
- diff.newNativeEnums.push(nativeEnum);
58
- }
59
- for (const [key, nativeEnum] of Object.entries(fromSchema.getNativeEnums())) {
60
- if (toSchema.hasNativeEnum(key)) {
61
- continue;
62
- }
63
- if (key.startsWith(`${fromSchema.name}.`) &&
64
- (fromSchema.name !== toSchema.name ||
65
- toSchema.getNativeEnum(key.substring(fromSchema.name.length + 1))?.schema === '*')) {
66
- continue;
67
- }
68
- diff.removedNativeEnums.push(nativeEnum);
69
- }
70
- for (const table of toSchema.getTables()) {
71
- const tableName = table.getShortestName(false);
72
- if (!fromSchema.hasTable(tableName)) {
73
- diff.newTables[tableName] = toSchema.getTable(tableName);
74
- }
75
- else {
76
- const tableDifferences = this.diffTable(fromSchema.getTable(tableName), toSchema.getTable(tableName), inverseDiff?.changedTables[tableName]);
77
- if (tableDifferences !== false) {
78
- diff.changedTables[tableName] = tableDifferences;
79
- }
80
- }
81
- }
82
- // Check if there are tables removed
83
- for (let table of fromSchema.getTables()) {
84
- const tableName = table.getShortestName();
85
- table = fromSchema.getTable(tableName);
86
- if (!toSchema.hasTable(tableName)) {
87
- diff.removedTables[tableName] = table;
88
- }
89
- // also remember all foreign keys that point to a specific table
90
- for (const foreignKey of Object.values(table.getForeignKeys())) {
91
- if (!foreignKeysToTable[foreignKey.referencedTableName]) {
92
- foreignKeysToTable[foreignKey.referencedTableName] = [];
93
- }
94
- foreignKeysToTable[foreignKey.referencedTableName].push(foreignKey);
95
- }
96
- }
97
- for (const table of Object.values(diff.removedTables)) {
98
- const tableName = (table.schema ? table.schema + '.' : '') + table.name;
99
- if (!foreignKeysToTable[tableName]) {
100
- continue;
101
- }
102
- diff.orphanedForeignKeys.push(...foreignKeysToTable[tableName]);
103
- // Deleting duplicated foreign keys present both on the orphanedForeignKey and the removedForeignKeys from changedTables.
104
- for (const foreignKey of foreignKeysToTable[tableName]) {
105
- const localTableName = foreignKey.localTableName;
106
- if (!diff.changedTables[localTableName]) {
107
- continue;
108
- }
109
- for (const [key, fk] of Object.entries(diff.changedTables[localTableName].removedForeignKeys)) {
110
- // We check if the key is from the removed table, if not we skip.
111
- if (tableName !== fk.referencedTableName) {
112
- continue;
113
- }
114
- delete diff.changedTables[localTableName].removedForeignKeys[key];
115
- }
116
- }
117
- }
118
- // Compare views — prefer schema-qualified lookup to avoid matching
119
- // views with the same name in different schemas
120
- for (const toView of toSchema.getViews()) {
121
- const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
122
- if (!fromSchema.hasView(viewName) && !fromSchema.hasView(toView.name)) {
123
- diff.newViews[viewName] = toView;
124
- this.log(`view ${viewName} added`);
125
- }
126
- else {
127
- const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name);
128
- if (fromView && this.diffViewExpression(fromView.definition, toView.definition)) {
129
- diff.changedViews[viewName] = { from: fromView, to: toView };
130
- this.log(`view ${viewName} changed`);
131
- }
132
- }
133
- }
134
- // Check for removed views
135
- for (const fromView of fromSchema.getViews()) {
136
- const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name;
137
- if (!toSchema.hasView(viewName) && !toSchema.hasView(fromView.name)) {
138
- diff.removedViews[viewName] = fromView;
139
- this.log(`view ${viewName} removed`);
140
- }
141
- }
142
- // Diff materialized view indexes using the existing table diff infrastructure.
143
- // Build transient DatabaseTable objects from view indexes so diffTable handles
144
- // added/removed/changed/renamed index detection and alterTable emits correct DDL.
145
- for (const toView of toSchema.getViews()) {
146
- if (!toView.materialized || toView.withData === false) {
147
- continue;
148
- }
149
- const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
150
- // New or definition-changed views have indexes handled during create/recreate
151
- if (diff.newViews[viewName] || diff.changedViews[viewName]) {
152
- continue;
153
- }
154
- // If we get here, the view exists in fromSchema (otherwise it would be in diff.newViews)
155
- const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name);
156
- const fromTable = new DatabaseTable(this.#platform, fromView.name, fromView.schema);
157
- fromTable.init([], fromView.indexes ?? [], [], []);
158
- const toTable = new DatabaseTable(this.#platform, toView.name, toView.schema);
159
- toTable.init([], toView.indexes ?? [], [], []);
160
- const tableDiff = this.diffTable(fromTable, toTable);
161
- if (tableDiff) {
162
- diff.changedTables[viewName] = tableDiff;
163
- }
164
- }
165
- return diff;
166
- }
167
- /**
168
- * Returns the difference between the tables fromTable and toTable.
169
- * If there are no differences this method returns the boolean false.
170
- */
171
- diffTable(fromTable, toTable, inverseTableDiff) {
172
- let changes = 0;
173
- const tableDifferences = {
174
- name: fromTable.getShortestName(),
175
- addedColumns: {},
176
- addedForeignKeys: {},
177
- addedIndexes: {},
178
- addedChecks: {},
179
- changedColumns: {},
180
- changedForeignKeys: {},
181
- changedIndexes: {},
182
- changedChecks: {},
183
- removedColumns: {},
184
- removedForeignKeys: {},
185
- removedIndexes: {},
186
- removedChecks: {},
187
- renamedColumns: {},
188
- renamedIndexes: {},
189
- fromTable,
190
- toTable,
191
- };
192
- if (this.diffComment(fromTable.comment, toTable.comment)) {
193
- tableDifferences.changedComment = toTable.comment;
194
- this.log(`table comment changed for ${tableDifferences.name}`, {
195
- fromTableComment: fromTable.comment,
196
- toTableComment: toTable.comment,
197
- });
198
- changes++;
199
- }
200
- const fromTableColumns = fromTable.getColumns();
201
- const toTableColumns = toTable.getColumns();
202
- // See if all the columns in "from" table exist in "to" table
203
- for (const column of toTableColumns) {
204
- if (fromTable.hasColumn(column.name)) {
205
- continue;
206
- }
207
- tableDifferences.addedColumns[column.name] = column;
208
- this.log(`column ${tableDifferences.name}.${column.name} of type ${column.type} added`);
209
- changes++;
210
- }
211
- /* See if there are any removed columns in "to" table */
212
- for (const column of fromTableColumns) {
213
- // See if column is removed in "to" table.
214
- if (!toTable.hasColumn(column.name)) {
215
- tableDifferences.removedColumns[column.name] = column;
216
- this.log(`column ${tableDifferences.name}.${column.name} removed`);
217
- changes++;
218
- continue;
219
- }
220
- // See if column has changed properties in "to" table.
221
- const changedProperties = this.diffColumn(column, toTable.getColumn(column.name), fromTable, true);
222
- if (changedProperties.size === 0) {
223
- continue;
224
- }
225
- if (changedProperties.size === 1 && changedProperties.has('generated')) {
226
- tableDifferences.addedColumns[column.name] = toTable.getColumn(column.name);
227
- tableDifferences.removedColumns[column.name] = column;
228
- changes++;
229
- continue;
230
- }
231
- tableDifferences.changedColumns[column.name] = {
232
- oldColumnName: column.name,
233
- fromColumn: column,
234
- column: toTable.getColumn(column.name),
235
- changedProperties,
236
- };
237
- this.log(`column ${tableDifferences.name}.${column.name} changed`, { changedProperties });
238
- changes++;
239
- }
240
- this.detectColumnRenamings(tableDifferences, inverseTableDiff);
241
- const fromTableIndexes = fromTable.getIndexes();
242
- const toTableIndexes = toTable.getIndexes();
243
- // See if all the indexes in "from" table exist in "to" table
244
- for (const index of Object.values(toTableIndexes)) {
245
- if ((index.primary && fromTableIndexes.find(i => i.primary)) || fromTable.hasIndex(index.keyName)) {
246
- continue;
247
- }
248
- tableDifferences.addedIndexes[index.keyName] = index;
249
- this.log(`index ${index.keyName} added to table ${tableDifferences.name}`, { index });
250
- changes++;
251
- }
252
- // See if there are any removed indexes in "to" table
253
- for (const index of fromTableIndexes) {
254
- // See if index is removed in "to" table.
255
- if ((index.primary && !toTable.hasPrimaryKey()) || (!index.primary && !toTable.hasIndex(index.keyName))) {
256
- tableDifferences.removedIndexes[index.keyName] = index;
257
- this.log(`index ${index.keyName} removed from table ${tableDifferences.name}`);
258
- changes++;
259
- continue;
260
- }
261
- // See if index has changed in "to" table.
262
- const toTableIndex = index.primary ? toTable.getPrimaryKey() : toTable.getIndex(index.keyName);
263
- if (!this.diffIndex(index, toTableIndex)) {
264
- continue;
265
- }
266
- tableDifferences.changedIndexes[index.keyName] = toTableIndex;
267
- this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
268
- fromTableIndex: index,
269
- toTableIndex,
270
- });
271
- changes++;
272
- }
273
- this.detectIndexRenamings(tableDifferences);
274
- const fromTableChecks = fromTable.getChecks();
275
- const toTableChecks = toTable.getChecks();
276
- // See if all the checks in "from" table exist in "to" table
277
- for (const check of toTableChecks) {
278
- if (fromTable.hasCheck(check.name)) {
279
- continue;
280
- }
281
- tableDifferences.addedChecks[check.name] = check;
282
- this.log(`check constraint ${check.name} added to table ${tableDifferences.name}`, { check });
283
- changes++;
284
- }
285
- // See if there are any removed checks in "to" table
286
- for (const check of fromTableChecks) {
287
- if (!toTable.hasCheck(check.name)) {
288
- tableDifferences.removedChecks[check.name] = check;
289
- this.log(`check constraint ${check.name} removed from table ${tableDifferences.name}`);
290
- changes++;
291
- continue;
292
- }
293
- // See if check has changed in "to" table
294
- const toTableCheck = toTable.getCheck(check.name);
295
- const toColumn = toTable.getColumn(check.columnName);
296
- const fromColumn = fromTable.getColumn(check.columnName);
297
- if (!this.diffExpression(check.expression, toTableCheck.expression)) {
298
- continue;
299
- }
300
- if (fromColumn?.enumItems &&
301
- toColumn?.enumItems &&
302
- !this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
303
- continue;
304
- }
305
- this.log(`check constraint ${check.name} changed in table ${tableDifferences.name}`, {
306
- fromTableCheck: check,
307
- toTableCheck,
308
- });
309
- tableDifferences.changedChecks[check.name] = toTableCheck;
310
- changes++;
311
- }
312
- const fromForeignKeys = { ...fromTable.getForeignKeys() };
313
- const toForeignKeys = { ...toTable.getForeignKeys() };
314
- for (const fromConstraint of Object.values(fromForeignKeys)) {
315
- for (const toConstraint of Object.values(toForeignKeys)) {
316
- if (!this.diffForeignKey(fromConstraint, toConstraint, tableDifferences)) {
317
- delete fromForeignKeys[fromConstraint.constraintName];
318
- delete toForeignKeys[toConstraint.constraintName];
319
- }
320
- else if (fromConstraint.constraintName.toLowerCase() === toConstraint.constraintName.toLowerCase()) {
321
- this.log(`FK constraint ${fromConstraint.constraintName} changed in table ${tableDifferences.name}`, {
322
- fromConstraint,
323
- toConstraint,
324
- });
325
- tableDifferences.changedForeignKeys[toConstraint.constraintName] = toConstraint;
326
- changes++;
327
- delete fromForeignKeys[fromConstraint.constraintName];
328
- delete toForeignKeys[toConstraint.constraintName];
329
- }
330
- }
331
- }
332
- for (const fromConstraint of Object.values(fromForeignKeys)) {
333
- tableDifferences.removedForeignKeys[fromConstraint.constraintName] = fromConstraint;
334
- this.log(`FK constraint ${fromConstraint.constraintName} removed from table ${tableDifferences.name}`);
335
- changes++;
336
- }
337
- for (const toConstraint of Object.values(toForeignKeys)) {
338
- tableDifferences.addedForeignKeys[toConstraint.constraintName] = toConstraint;
339
- this.log(`FK constraint ${toConstraint.constraintName} added to table ${tableDifferences.name}`, {
340
- constraint: toConstraint,
341
- });
342
- changes++;
343
- }
344
- return changes ? tableDifferences : false;
345
- }
346
- /**
347
- * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
348
- * however ambiguities between different possibilities should not lead to renaming at all.
349
- */
350
- detectColumnRenamings(tableDifferences, inverseTableDiff) {
351
- const renameCandidates = {};
352
- const oldFKs = Object.values(tableDifferences.fromTable.getForeignKeys());
353
- const newFKs = Object.values(tableDifferences.toTable.getForeignKeys());
354
- for (const addedColumn of Object.values(tableDifferences.addedColumns)) {
355
- for (const removedColumn of Object.values(tableDifferences.removedColumns)) {
356
- const diff = this.diffColumn(addedColumn, removedColumn, tableDifferences.fromTable);
357
- if (diff.size !== 0) {
358
- continue;
359
- }
360
- const wasFK = oldFKs.some(fk => fk.columnNames.includes(removedColumn.name));
361
- const isFK = newFKs.some(fk => fk.columnNames.includes(addedColumn.name));
362
- if (wasFK !== isFK) {
363
- continue;
364
- }
365
- const renamedColumn = inverseTableDiff?.renamedColumns[addedColumn.name];
366
- if (renamedColumn && renamedColumn?.name !== removedColumn.name) {
367
- continue;
368
- }
369
- renameCandidates[addedColumn.name] = renameCandidates[addedColumn.name] ?? [];
370
- renameCandidates[addedColumn.name].push([removedColumn, addedColumn]);
371
- }
372
- }
373
- for (const candidateColumns of Object.values(renameCandidates)) {
374
- if (candidateColumns.length !== 1) {
375
- continue;
376
- }
377
- const [removedColumn, addedColumn] = candidateColumns[0];
378
- const removedColumnName = removedColumn.name;
379
- const addedColumnName = addedColumn.name;
380
- /* v8 ignore next */
381
- if (tableDifferences.renamedColumns[removedColumnName]) {
382
- continue;
383
- }
384
- tableDifferences.renamedColumns[removedColumnName] = addedColumn;
385
- delete tableDifferences.addedColumns[addedColumnName];
386
- delete tableDifferences.removedColumns[removedColumnName];
387
- this.log(`renamed column detected in table ${tableDifferences.name}`, {
388
- old: removedColumnName,
389
- new: addedColumnName,
390
- });
391
- }
7
+ #helper;
8
+ #logger;
9
+ #platform;
10
+ constructor(platform) {
11
+ this.#platform = platform;
12
+ this.#helper = this.#platform.getSchemaHelper();
13
+ this.#logger = this.#platform.getConfig().getLogger();
14
+ }
15
+ /**
16
+ * Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema.
17
+ *
18
+ * The returned differences are returned in such a way that they contain the
19
+ * operations to change the schema stored in fromSchema to the schema that is
20
+ * stored in toSchema.
21
+ */
22
+ compare(fromSchema, toSchema, inverseDiff) {
23
+ const diff = {
24
+ newTables: {},
25
+ removedTables: {},
26
+ changedTables: {},
27
+ newViews: {},
28
+ changedViews: {},
29
+ removedViews: {},
30
+ orphanedForeignKeys: [],
31
+ newNativeEnums: [],
32
+ removedNativeEnums: [],
33
+ newNamespaces: new Set(),
34
+ removedNamespaces: new Set(),
35
+ fromSchema,
36
+ };
37
+ const foreignKeysToTable = {};
38
+ for (const namespace of toSchema.getNamespaces()) {
39
+ if (fromSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
40
+ continue;
41
+ }
42
+ diff.newNamespaces.add(namespace);
392
43
  }
393
- /**
394
- * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
395
- * however ambiguities between different possibilities should not lead to renaming at all.
396
- */
397
- detectIndexRenamings(tableDifferences) {
398
- const renameCandidates = {};
399
- // Gather possible rename candidates by comparing each added and removed index based on semantics.
400
- for (const addedIndex of Object.values(tableDifferences.addedIndexes)) {
401
- for (const removedIndex of Object.values(tableDifferences.removedIndexes)) {
402
- if (this.diffIndex(addedIndex, removedIndex)) {
403
- continue;
404
- }
405
- renameCandidates[addedIndex.keyName] = renameCandidates[addedIndex.keyName] ?? [];
406
- renameCandidates[addedIndex.keyName].push([removedIndex, addedIndex]);
407
- }
408
- }
409
- for (const candidateIndexes of Object.values(renameCandidates)) {
410
- // If the current rename candidate contains exactly one semantically equal index, we can safely rename it.
411
- // Otherwise it is unclear if a rename action is really intended, therefore we let those ambiguous indexes be added/dropped.
412
- if (candidateIndexes.length !== 1) {
413
- continue;
414
- }
415
- const [removedIndex, addedIndex] = candidateIndexes[0];
416
- const removedIndexName = removedIndex.keyName;
417
- const addedIndexName = addedIndex.keyName;
418
- if (tableDifferences.renamedIndexes[removedIndexName]) {
419
- continue;
420
- }
421
- tableDifferences.renamedIndexes[removedIndexName] = addedIndex;
422
- delete tableDifferences.addedIndexes[addedIndexName];
423
- delete tableDifferences.removedIndexes[removedIndexName];
424
- this.log(`renamed index detected in table ${tableDifferences.name}`, {
425
- old: removedIndexName,
426
- new: addedIndexName,
427
- });
428
- }
44
+ for (const namespace of fromSchema.getNamespaces()) {
45
+ if (toSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
46
+ continue;
47
+ }
48
+ diff.removedNamespaces.add(namespace);
429
49
  }
430
- diffForeignKey(key1, key2, tableDifferences) {
431
- if (key1.columnNames.join('~').toLowerCase() !== key2.columnNames.join('~').toLowerCase()) {
432
- return true;
433
- }
434
- if (key1.referencedColumnNames.join('~').toLowerCase() !== key2.referencedColumnNames.join('~').toLowerCase()) {
435
- return true;
436
- }
437
- if (key1.constraintName !== key2.constraintName) {
438
- return true;
439
- }
440
- if (key1.referencedTableName !== key2.referencedTableName) {
441
- return true;
442
- }
443
- if (key1.deferMode !== key2.deferMode) {
444
- return true;
445
- }
446
- if (key1.localTableName === key1.referencedTableName && !this.#platform.supportsMultipleCascadePaths()) {
447
- return false;
448
- }
449
- if (key1.columnNames.some(col => tableDifferences.changedColumns[col]?.changedProperties.has('type'))) {
450
- return true;
451
- }
452
- const defaultRule = ['restrict', 'no action'];
453
- const rule = (key, method) => {
454
- return (key[method] ?? defaultRule[0]).toLowerCase().replace(defaultRule[1], defaultRule[0]).replace(/"/g, '');
455
- };
456
- const compare = (method) => rule(key1, method) === rule(key2, method);
457
- // Skip updateRule comparison for platforms that don't support ON UPDATE (e.g., Oracle)
458
- const updateRuleDiffers = this.#platform.supportsOnUpdate() && !compare('updateRule');
459
- return updateRuleDiffers || !compare('deleteRule');
460
- }
461
- /**
462
- * Returns the difference between the columns
463
- */
464
- diffColumn(fromColumn, toColumn, fromTable, logging) {
465
- const changedProperties = new Set();
466
- const fromProp = this.mapColumnToProperty({ ...fromColumn, autoincrement: false });
467
- const toProp = this.mapColumnToProperty({ ...toColumn, autoincrement: false });
468
- const fromColumnType = this.#platform.normalizeColumnType(fromColumn.mappedType.getColumnType(fromProp, this.#platform).toLowerCase(), fromProp);
469
- const fromNativeEnum = fromTable.nativeEnums[fromColumnType] ??
470
- Object.values(fromTable.nativeEnums).find(e => e.name === fromColumnType && e.schema !== '*');
471
- let toColumnType = this.#platform.normalizeColumnType(toColumn.mappedType.getColumnType(toProp, this.#platform).toLowerCase(), toProp);
472
- const log = (msg, params) => {
473
- if (logging) {
474
- const copy = Utils.copy(params);
475
- Utils.dropUndefinedProperties(copy);
476
- this.log(msg, copy);
477
- }
478
- };
479
- if (fromColumnType !== toColumnType &&
480
- (!fromNativeEnum || `${fromNativeEnum.schema}.${fromNativeEnum.name}` !== toColumnType) &&
481
- !(fromColumn.ignoreSchemaChanges?.includes('type') || toColumn.ignoreSchemaChanges?.includes('type')) &&
482
- !fromColumn.generated &&
483
- !toColumn.generated) {
484
- if (!toColumnType.includes('.') &&
485
- fromTable.schema &&
486
- fromTable.schema !== this.#platform.getDefaultSchemaName()) {
487
- toColumnType = `${fromTable.schema}.${toColumnType}`;
488
- }
489
- if (fromColumnType !== toColumnType) {
490
- log(`'type' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumnType, toColumnType });
491
- changedProperties.add('type');
492
- }
493
- }
494
- if (!!fromColumn.nullable !== !!toColumn.nullable && !fromColumn.generated && !toColumn.generated) {
495
- log(`'nullable' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
496
- changedProperties.add('nullable');
497
- }
498
- if (this.diffExpression(fromColumn.generated, toColumn.generated)) {
499
- log(`'generated' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
500
- changedProperties.add('generated');
501
- }
502
- if (!!fromColumn.autoincrement !== !!toColumn.autoincrement) {
503
- log(`'autoincrement' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
504
- changedProperties.add('autoincrement');
505
- }
506
- if (!!fromColumn.unsigned !== !!toColumn.unsigned && this.#platform.supportsUnsigned()) {
507
- log(`'unsigned' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
508
- changedProperties.add('unsigned');
509
- }
510
- if (!(fromColumn.ignoreSchemaChanges?.includes('default') || toColumn.ignoreSchemaChanges?.includes('default')) &&
511
- !this.hasSameDefaultValue(fromColumn, toColumn)) {
512
- log(`'default' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
513
- changedProperties.add('default');
514
- }
515
- if (this.diffComment(fromColumn.comment, toColumn.comment)) {
516
- log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
517
- changedProperties.add('comment');
518
- }
519
- if (!(fromColumn.mappedType instanceof ArrayType) &&
520
- !(toColumn.mappedType instanceof ArrayType) &&
521
- this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
522
- log(`'enumItems' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
523
- changedProperties.add('enumItems');
524
- }
525
- if ((fromColumn.extra || '').toLowerCase() !== (toColumn.extra || '').toLowerCase() &&
526
- !(fromColumn.ignoreSchemaChanges?.includes('extra') || toColumn.ignoreSchemaChanges?.includes('extra'))) {
527
- log(`'extra' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
528
- changedProperties.add('extra');
529
- }
530
- return changedProperties;
531
- }
532
- diffEnumItems(items1 = [], items2 = []) {
533
- return items1.length !== items2.length || items1.some((v, i) => v !== items2[i]);
534
- }
535
- diffComment(comment1, comment2) {
536
- // A null value and an empty string are actually equal for a comment so they should not trigger a change.
537
- // eslint-disable-next-line eqeqeq
538
- return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
539
- }
540
- /**
541
- * Finds the difference between the indexes index1 and index2.
542
- * Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
543
- */
544
- diffIndex(index1, index2) {
545
- // if one of them is a custom expression or full text index, compare only by name
546
- if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
547
- return index1.keyName !== index2.keyName;
548
- }
549
- return !this.isIndexFulfilledBy(index1, index2) || !this.isIndexFulfilledBy(index2, index1);
550
- }
551
- /**
552
- * Checks if the other index already fulfills all the indexing and constraint needs of the current one.
553
- */
554
- isIndexFulfilledBy(index1, index2) {
555
- // allow the other index to be equally large only. It being larger is an option but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
556
- if (index1.columnNames.length !== index2.columnNames.length) {
557
- return false;
558
- }
559
- function spansColumns() {
560
- for (let i = 0; i < index1.columnNames.length; i++) {
561
- if (index1.columnNames[i] === index2.columnNames[i]) {
562
- continue;
563
- }
564
- return false;
565
- }
566
- return true;
567
- }
568
- // Check if columns are the same, and even in the same order
569
- if (!spansColumns()) {
570
- return false;
571
- }
572
- // Compare advanced column options (sort order, nulls, length, collation)
573
- if (!this.compareIndexColumns(index1, index2)) {
574
- return false;
575
- }
576
- // Compare INCLUDE columns for covering indexes
577
- if (!this.compareArrays(index1.include, index2.include)) {
578
- return false;
579
- }
580
- // Compare fill factor
581
- if (index1.fillFactor !== index2.fillFactor) {
582
- return false;
583
- }
584
- // Compare invisible flag
585
- if (!!index1.invisible !== !!index2.invisible) {
586
- return false;
587
- }
588
- // Compare disabled flag
589
- if (!!index1.disabled !== !!index2.disabled) {
590
- return false;
591
- }
592
- // Compare clustered flag
593
- if (!!index1.clustered !== !!index2.clustered) {
594
- return false;
595
- }
596
- if (!index1.unique && !index1.primary) {
597
- // this is a special case: If the current key is neither primary or unique, any unique or
598
- // primary key will always have the same effect for the index and there cannot be any constraint
599
- // overlaps. This means a primary or unique index can always fulfill the requirements of just an
600
- // index that has no constraints.
601
- return true;
602
- }
603
- if (this.#platform.supportsDeferredUniqueConstraints() && index1.deferMode !== index2.deferMode) {
604
- return false;
605
- }
606
- return index1.primary === index2.primary && index1.unique === index2.unique;
607
- }
608
- /**
609
- * Compare advanced column options between two indexes.
610
- */
611
- compareIndexColumns(index1, index2) {
612
- const cols1 = index1.columns ?? [];
613
- const cols2 = index2.columns ?? [];
614
- // If neither has column options, they match
615
- if (cols1.length === 0 && cols2.length === 0) {
616
- return true;
617
- }
618
- // If only one has column options, they don't match
619
- if (cols1.length !== cols2.length) {
620
- return false;
621
- }
622
- // Compare each column's options
623
- // Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
624
- // and the columns array is derived from those same column names
625
- for (let i = 0; i < cols1.length; i++) {
626
- const c1 = cols1[i];
627
- const c2 = cols2[i];
628
- const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
629
- const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
630
- if (sort1 !== sort2) {
631
- return false;
632
- }
633
- const defaultNulls = (s) => (s === 'DESC' ? 'FIRST' : 'LAST');
634
- const nulls1 = c1.nulls?.toUpperCase() ?? defaultNulls(sort1);
635
- const nulls2 = c2.nulls?.toUpperCase() ?? defaultNulls(sort2);
636
- if (nulls1 !== nulls2) {
637
- return false;
638
- }
639
- if (c1.length !== c2.length) {
640
- return false;
641
- }
642
- if (c1.collation !== c2.collation) {
643
- return false;
644
- }
645
- }
646
- return true;
647
- }
648
- /**
649
- * Compare two arrays for equality (order matters).
650
- */
651
- compareArrays(arr1, arr2) {
652
- if (!arr1 && !arr2) {
653
- return true;
654
- }
655
- if (!arr1 || !arr2 || arr1.length !== arr2.length) {
656
- return false;
657
- }
658
- return arr1.every((val, i) => val === arr2[i]);
659
- }
660
- diffExpression(expr1, expr2) {
661
- // expressions like check constraints might be normalized by the driver,
662
- // e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
663
- const simplify = (str) => {
664
- return (str
665
- ?.replace(/_\w+'(.*?)'/g, '$1')
666
- .replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
667
- // MySQL normalizes count(*) to count(0)
668
- .replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
669
- // Remove quotes first so we can process identifiers
670
- .replace(/['"`]/g, '')
671
- // MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
672
- // Strip these prefixes - match word.word patterns and keep only the last part
673
- .replace(/\b\w+\.(\w+)/g, '$1')
674
- // Normalize JOIN syntax: inner join -> join (equivalent in SQL)
675
- .replace(/\binner\s+join\b/gi, 'join')
676
- // Remove redundant column aliases like `title AS title` -> `title`
677
- .replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
678
- // Remove AS keyword (optional in SQL, MySQL may add/remove it)
679
- .replace(/\bas\b/gi, '')
680
- // Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
681
- .replace(/[()\n[\]*]|::\w+| +/g, '')
682
- .replace(/anyarray\[(.*)]/gi, '$1')
683
- .toLowerCase()
684
- // PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
685
- // After removing AS and whitespace, this results in duplicate adjacent words
686
- // Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
687
- // Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
688
- .replace(/(\w{3,})\1/g, '$1')
689
- // Remove trailing semicolon (PostgreSQL adds it to view definitions)
690
- .replace(/;$/, ''));
691
- };
692
- return simplify(expr1) !== simplify(expr2);
693
- }
694
- /**
695
- * Compares two view expressions, with special handling for SELECT *.
696
- * Databases like PostgreSQL and MySQL expand `SELECT *` to explicit column names
697
- * in their stored view definitions, which makes diffExpression always detect changes.
698
- * When SELECT * is present, we strip the first SELECT...FROM column list from both
699
- * sides and compare only the structural parts (FROM clause onwards).
700
- * Note: this means changes *within* subqueries in the SELECT list of a SELECT * view
701
- * may not be detected — an acceptable tradeoff since SELECT * views are inherently
702
- * column-list-agnostic.
703
- * @see https://github.com/mikro-orm/mikro-orm/issues/7308
704
- */
705
- diffViewExpression(fromDef, toDef) {
706
- if (!this.diffExpression(fromDef, toDef)) {
707
- return false;
708
- }
709
- // If either expression uses SELECT *, the diff may be due to * expansion
710
- if (/\bselect\s+\*/i.test(fromDef) || /\bselect\s+\*/i.test(toDef)) {
711
- const stripColumns = (s) => s.replace(/\bselect\b[\s\S]*?\bfrom\b/i, 'select from');
712
- return this.diffExpression(stripColumns(fromDef), stripColumns(toDef));
713
- }
714
- return true;
50
+ for (const [key, nativeEnum] of Object.entries(toSchema.getNativeEnums())) {
51
+ if (fromSchema.hasNativeEnum(key)) {
52
+ continue;
53
+ }
54
+ if (nativeEnum.schema === '*' && fromSchema.hasNativeEnum(`${toSchema.name}.${key}`)) {
55
+ continue;
56
+ }
57
+ diff.newNativeEnums.push(nativeEnum);
715
58
  }
716
- parseJsonDefault(defaultValue) {
717
- /* v8 ignore next */
718
- if (!defaultValue) {
719
- return null;
720
- }
721
- const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
722
- return parseJsonSafe(val);
723
- }
724
- hasSameDefaultValue(from, to) {
725
- if (from.default == null ||
726
- from.default.toString().toLowerCase() === 'null' ||
727
- from.default.toString().startsWith('nextval(')) {
728
- return to.default == null || to.default.toLowerCase() === 'null';
729
- }
730
- if (to.mappedType instanceof BooleanType) {
731
- const defaultValueFrom = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + from.default);
732
- const defaultValueTo = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + to.default);
733
- return defaultValueFrom === defaultValueTo;
734
- }
735
- if (to.mappedType instanceof JsonType) {
736
- const defaultValueFrom = this.parseJsonDefault(from.default);
737
- const defaultValueTo = this.parseJsonDefault(to.default);
738
- return Utils.equals(defaultValueFrom, defaultValueTo);
739
- }
740
- if (to.mappedType instanceof DateTimeType && from.default && to.default) {
741
- // normalize now/current_timestamp defaults, also remove `()` from the end of default expression
742
- const defaultValueFrom = from.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
743
- const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
744
- return defaultValueFrom === defaultValueTo;
745
- }
746
- if (from.default && to.default) {
747
- return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
748
- }
749
- if (['', this.#helper.getDefaultEmptyString()].includes(to.default) && from.default != null) {
750
- return ['', this.#helper.getDefaultEmptyString()].includes(from.default.toString());
751
- }
752
- // eslint-disable-next-line eqeqeq
753
- return from.default == to.default; // == intentionally
754
- }
755
- mapColumnToProperty(column) {
756
- const length = /\w+\((\d+)\)/.exec(column.type);
757
- const match = /\w+\((\d+), ?(\d+)\)/.exec(column.type);
758
- return {
759
- fieldNames: [column.name],
760
- columnTypes: [column.type],
761
- items: column.enumItems,
762
- ...column,
763
- length: length ? +length[1] : column.length,
764
- precision: match ? +match[1] : column.precision,
765
- scale: match ? +match[2] : column.scale,
766
- };
767
- }
768
- log(message, params) {
769
- if (params) {
770
- message += ' ' + inspect(params);
771
- }
772
- this.#logger.log('schema', message);
59
+ for (const [key, nativeEnum] of Object.entries(fromSchema.getNativeEnums())) {
60
+ if (toSchema.hasNativeEnum(key)) {
61
+ continue;
62
+ }
63
+ if (
64
+ key.startsWith(`${fromSchema.name}.`) &&
65
+ (fromSchema.name !== toSchema.name ||
66
+ toSchema.getNativeEnum(key.substring(fromSchema.name.length + 1))?.schema === '*')
67
+ ) {
68
+ continue;
69
+ }
70
+ diff.removedNativeEnums.push(nativeEnum);
71
+ }
72
+ for (const table of toSchema.getTables()) {
73
+ const tableName = table.getShortestName(false);
74
+ if (!fromSchema.hasTable(tableName)) {
75
+ diff.newTables[tableName] = toSchema.getTable(tableName);
76
+ } else {
77
+ const tableDifferences = this.diffTable(
78
+ fromSchema.getTable(tableName),
79
+ toSchema.getTable(tableName),
80
+ inverseDiff?.changedTables[tableName],
81
+ );
82
+ if (tableDifferences !== false) {
83
+ diff.changedTables[tableName] = tableDifferences;
84
+ }
85
+ }
86
+ }
87
+ // Check if there are tables removed
88
+ for (let table of fromSchema.getTables()) {
89
+ const tableName = table.getShortestName();
90
+ table = fromSchema.getTable(tableName);
91
+ if (!toSchema.hasTable(tableName)) {
92
+ diff.removedTables[tableName] = table;
93
+ }
94
+ // also remember all foreign keys that point to a specific table
95
+ for (const foreignKey of Object.values(table.getForeignKeys())) {
96
+ if (!foreignKeysToTable[foreignKey.referencedTableName]) {
97
+ foreignKeysToTable[foreignKey.referencedTableName] = [];
98
+ }
99
+ foreignKeysToTable[foreignKey.referencedTableName].push(foreignKey);
100
+ }
101
+ }
102
+ for (const table of Object.values(diff.removedTables)) {
103
+ const tableName = (table.schema ? table.schema + '.' : '') + table.name;
104
+ if (!foreignKeysToTable[tableName]) {
105
+ continue;
106
+ }
107
+ diff.orphanedForeignKeys.push(...foreignKeysToTable[tableName]);
108
+ // Deleting duplicated foreign keys present both on the orphanedForeignKey and the removedForeignKeys from changedTables.
109
+ for (const foreignKey of foreignKeysToTable[tableName]) {
110
+ const localTableName = foreignKey.localTableName;
111
+ if (!diff.changedTables[localTableName]) {
112
+ continue;
113
+ }
114
+ for (const [key, fk] of Object.entries(diff.changedTables[localTableName].removedForeignKeys)) {
115
+ // We check if the key is from the removed table, if not we skip.
116
+ if (tableName !== fk.referencedTableName) {
117
+ continue;
118
+ }
119
+ delete diff.changedTables[localTableName].removedForeignKeys[key];
120
+ }
121
+ }
122
+ }
123
+ // Compare views — prefer schema-qualified lookup to avoid matching
124
+ // views with the same name in different schemas
125
+ for (const toView of toSchema.getViews()) {
126
+ const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
127
+ if (!fromSchema.hasView(viewName) && !fromSchema.hasView(toView.name)) {
128
+ diff.newViews[viewName] = toView;
129
+ this.log(`view ${viewName} added`);
130
+ } else {
131
+ const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name);
132
+ if (fromView && this.diffViewExpression(fromView.definition, toView.definition)) {
133
+ diff.changedViews[viewName] = { from: fromView, to: toView };
134
+ this.log(`view ${viewName} changed`);
135
+ }
136
+ }
137
+ }
138
+ // Check for removed views
139
+ for (const fromView of fromSchema.getViews()) {
140
+ const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name;
141
+ if (!toSchema.hasView(viewName) && !toSchema.hasView(fromView.name)) {
142
+ diff.removedViews[viewName] = fromView;
143
+ this.log(`view ${viewName} removed`);
144
+ }
145
+ }
146
+ // Diff materialized view indexes using the existing table diff infrastructure.
147
+ // Build transient DatabaseTable objects from view indexes so diffTable handles
148
+ // added/removed/changed/renamed index detection and alterTable emits correct DDL.
149
+ for (const toView of toSchema.getViews()) {
150
+ if (!toView.materialized || toView.withData === false) {
151
+ continue;
152
+ }
153
+ const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
154
+ // New or definition-changed views have indexes handled during create/recreate
155
+ if (diff.newViews[viewName] || diff.changedViews[viewName]) {
156
+ continue;
157
+ }
158
+ // If we get here, the view exists in fromSchema (otherwise it would be in diff.newViews)
159
+ const fromView = fromSchema.getView(viewName) ?? fromSchema.getView(toView.name);
160
+ const fromTable = new DatabaseTable(this.#platform, fromView.name, fromView.schema);
161
+ fromTable.init([], fromView.indexes ?? [], [], []);
162
+ const toTable = new DatabaseTable(this.#platform, toView.name, toView.schema);
163
+ toTable.init([], toView.indexes ?? [], [], []);
164
+ const tableDiff = this.diffTable(fromTable, toTable);
165
+ if (tableDiff) {
166
+ diff.changedTables[viewName] = tableDiff;
167
+ }
168
+ }
169
+ return diff;
170
+ }
171
+ /**
172
+ * Returns the difference between the tables fromTable and toTable.
173
+ * If there are no differences this method returns the boolean false.
174
+ */
175
+ diffTable(fromTable, toTable, inverseTableDiff) {
176
+ let changes = 0;
177
+ const tableDifferences = {
178
+ name: fromTable.getShortestName(),
179
+ addedColumns: {},
180
+ addedForeignKeys: {},
181
+ addedIndexes: {},
182
+ addedChecks: {},
183
+ changedColumns: {},
184
+ changedForeignKeys: {},
185
+ changedIndexes: {},
186
+ changedChecks: {},
187
+ removedColumns: {},
188
+ removedForeignKeys: {},
189
+ removedIndexes: {},
190
+ removedChecks: {},
191
+ renamedColumns: {},
192
+ renamedIndexes: {},
193
+ fromTable,
194
+ toTable,
195
+ };
196
+ if (this.diffComment(fromTable.comment, toTable.comment)) {
197
+ tableDifferences.changedComment = toTable.comment;
198
+ this.log(`table comment changed for ${tableDifferences.name}`, {
199
+ fromTableComment: fromTable.comment,
200
+ toTableComment: toTable.comment,
201
+ });
202
+ changes++;
203
+ }
204
+ const fromTableColumns = fromTable.getColumns();
205
+ const toTableColumns = toTable.getColumns();
206
+ // See if all the columns in "from" table exist in "to" table
207
+ for (const column of toTableColumns) {
208
+ if (fromTable.hasColumn(column.name)) {
209
+ continue;
210
+ }
211
+ tableDifferences.addedColumns[column.name] = column;
212
+ this.log(`column ${tableDifferences.name}.${column.name} of type ${column.type} added`);
213
+ changes++;
214
+ }
215
+ /* See if there are any removed columns in "to" table */
216
+ for (const column of fromTableColumns) {
217
+ // See if column is removed in "to" table.
218
+ if (!toTable.hasColumn(column.name)) {
219
+ tableDifferences.removedColumns[column.name] = column;
220
+ this.log(`column ${tableDifferences.name}.${column.name} removed`);
221
+ changes++;
222
+ continue;
223
+ }
224
+ // See if column has changed properties in "to" table.
225
+ const changedProperties = this.diffColumn(column, toTable.getColumn(column.name), fromTable, true);
226
+ if (changedProperties.size === 0) {
227
+ continue;
228
+ }
229
+ if (changedProperties.size === 1 && changedProperties.has('generated')) {
230
+ tableDifferences.addedColumns[column.name] = toTable.getColumn(column.name);
231
+ tableDifferences.removedColumns[column.name] = column;
232
+ changes++;
233
+ continue;
234
+ }
235
+ tableDifferences.changedColumns[column.name] = {
236
+ oldColumnName: column.name,
237
+ fromColumn: column,
238
+ column: toTable.getColumn(column.name),
239
+ changedProperties,
240
+ };
241
+ this.log(`column ${tableDifferences.name}.${column.name} changed`, { changedProperties });
242
+ changes++;
243
+ }
244
+ this.detectColumnRenamings(tableDifferences, inverseTableDiff);
245
+ const fromTableIndexes = fromTable.getIndexes();
246
+ const toTableIndexes = toTable.getIndexes();
247
+ // See if all the indexes in "from" table exist in "to" table
248
+ for (const index of Object.values(toTableIndexes)) {
249
+ if ((index.primary && fromTableIndexes.find(i => i.primary)) || fromTable.hasIndex(index.keyName)) {
250
+ continue;
251
+ }
252
+ tableDifferences.addedIndexes[index.keyName] = index;
253
+ this.log(`index ${index.keyName} added to table ${tableDifferences.name}`, { index });
254
+ changes++;
255
+ }
256
+ // See if there are any removed indexes in "to" table
257
+ for (const index of fromTableIndexes) {
258
+ // See if index is removed in "to" table.
259
+ if ((index.primary && !toTable.hasPrimaryKey()) || (!index.primary && !toTable.hasIndex(index.keyName))) {
260
+ tableDifferences.removedIndexes[index.keyName] = index;
261
+ this.log(`index ${index.keyName} removed from table ${tableDifferences.name}`);
262
+ changes++;
263
+ continue;
264
+ }
265
+ // See if index has changed in "to" table.
266
+ const toTableIndex = index.primary ? toTable.getPrimaryKey() : toTable.getIndex(index.keyName);
267
+ if (!this.diffIndex(index, toTableIndex)) {
268
+ continue;
269
+ }
270
+ tableDifferences.changedIndexes[index.keyName] = toTableIndex;
271
+ this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
272
+ fromTableIndex: index,
273
+ toTableIndex,
274
+ });
275
+ changes++;
276
+ }
277
+ this.detectIndexRenamings(tableDifferences);
278
+ const fromTableChecks = fromTable.getChecks();
279
+ const toTableChecks = toTable.getChecks();
280
+ // See if all the checks in "from" table exist in "to" table
281
+ for (const check of toTableChecks) {
282
+ if (fromTable.hasCheck(check.name)) {
283
+ continue;
284
+ }
285
+ tableDifferences.addedChecks[check.name] = check;
286
+ this.log(`check constraint ${check.name} added to table ${tableDifferences.name}`, { check });
287
+ changes++;
288
+ }
289
+ // See if there are any removed checks in "to" table
290
+ for (const check of fromTableChecks) {
291
+ if (!toTable.hasCheck(check.name)) {
292
+ tableDifferences.removedChecks[check.name] = check;
293
+ this.log(`check constraint ${check.name} removed from table ${tableDifferences.name}`);
294
+ changes++;
295
+ continue;
296
+ }
297
+ // See if check has changed in "to" table
298
+ const toTableCheck = toTable.getCheck(check.name);
299
+ const toColumn = toTable.getColumn(check.columnName);
300
+ const fromColumn = fromTable.getColumn(check.columnName);
301
+ if (!this.diffExpression(check.expression, toTableCheck.expression)) {
302
+ continue;
303
+ }
304
+ if (
305
+ fromColumn?.enumItems &&
306
+ toColumn?.enumItems &&
307
+ !this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)
308
+ ) {
309
+ continue;
310
+ }
311
+ this.log(`check constraint ${check.name} changed in table ${tableDifferences.name}`, {
312
+ fromTableCheck: check,
313
+ toTableCheck,
314
+ });
315
+ tableDifferences.changedChecks[check.name] = toTableCheck;
316
+ changes++;
317
+ }
318
+ const fromForeignKeys = { ...fromTable.getForeignKeys() };
319
+ const toForeignKeys = { ...toTable.getForeignKeys() };
320
+ for (const fromConstraint of Object.values(fromForeignKeys)) {
321
+ for (const toConstraint of Object.values(toForeignKeys)) {
322
+ if (!this.diffForeignKey(fromConstraint, toConstraint, tableDifferences)) {
323
+ delete fromForeignKeys[fromConstraint.constraintName];
324
+ delete toForeignKeys[toConstraint.constraintName];
325
+ } else if (fromConstraint.constraintName.toLowerCase() === toConstraint.constraintName.toLowerCase()) {
326
+ this.log(`FK constraint ${fromConstraint.constraintName} changed in table ${tableDifferences.name}`, {
327
+ fromConstraint,
328
+ toConstraint,
329
+ });
330
+ tableDifferences.changedForeignKeys[toConstraint.constraintName] = toConstraint;
331
+ changes++;
332
+ delete fromForeignKeys[fromConstraint.constraintName];
333
+ delete toForeignKeys[toConstraint.constraintName];
334
+ }
335
+ }
336
+ }
337
+ for (const fromConstraint of Object.values(fromForeignKeys)) {
338
+ tableDifferences.removedForeignKeys[fromConstraint.constraintName] = fromConstraint;
339
+ this.log(`FK constraint ${fromConstraint.constraintName} removed from table ${tableDifferences.name}`);
340
+ changes++;
341
+ }
342
+ for (const toConstraint of Object.values(toForeignKeys)) {
343
+ tableDifferences.addedForeignKeys[toConstraint.constraintName] = toConstraint;
344
+ this.log(`FK constraint ${toConstraint.constraintName} added to table ${tableDifferences.name}`, {
345
+ constraint: toConstraint,
346
+ });
347
+ changes++;
348
+ }
349
+ return changes ? tableDifferences : false;
350
+ }
351
+ /**
352
+ * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
353
+ * however ambiguities between different possibilities should not lead to renaming at all.
354
+ */
355
+ detectColumnRenamings(tableDifferences, inverseTableDiff) {
356
+ const renameCandidates = {};
357
+ const oldFKs = Object.values(tableDifferences.fromTable.getForeignKeys());
358
+ const newFKs = Object.values(tableDifferences.toTable.getForeignKeys());
359
+ for (const addedColumn of Object.values(tableDifferences.addedColumns)) {
360
+ for (const removedColumn of Object.values(tableDifferences.removedColumns)) {
361
+ const diff = this.diffColumn(addedColumn, removedColumn, tableDifferences.fromTable);
362
+ if (diff.size !== 0) {
363
+ continue;
364
+ }
365
+ const wasFK = oldFKs.some(fk => fk.columnNames.includes(removedColumn.name));
366
+ const isFK = newFKs.some(fk => fk.columnNames.includes(addedColumn.name));
367
+ if (wasFK !== isFK) {
368
+ continue;
369
+ }
370
+ const renamedColumn = inverseTableDiff?.renamedColumns[addedColumn.name];
371
+ if (renamedColumn && renamedColumn?.name !== removedColumn.name) {
372
+ continue;
373
+ }
374
+ renameCandidates[addedColumn.name] = renameCandidates[addedColumn.name] ?? [];
375
+ renameCandidates[addedColumn.name].push([removedColumn, addedColumn]);
376
+ }
377
+ }
378
+ for (const candidateColumns of Object.values(renameCandidates)) {
379
+ if (candidateColumns.length !== 1) {
380
+ continue;
381
+ }
382
+ const [removedColumn, addedColumn] = candidateColumns[0];
383
+ const removedColumnName = removedColumn.name;
384
+ const addedColumnName = addedColumn.name;
385
+ /* v8 ignore next */
386
+ if (tableDifferences.renamedColumns[removedColumnName]) {
387
+ continue;
388
+ }
389
+ tableDifferences.renamedColumns[removedColumnName] = addedColumn;
390
+ delete tableDifferences.addedColumns[addedColumnName];
391
+ delete tableDifferences.removedColumns[removedColumnName];
392
+ this.log(`renamed column detected in table ${tableDifferences.name}`, {
393
+ old: removedColumnName,
394
+ new: addedColumnName,
395
+ });
396
+ }
397
+ }
398
+ /**
399
+ * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
400
+ * however ambiguities between different possibilities should not lead to renaming at all.
401
+ */
402
+ detectIndexRenamings(tableDifferences) {
403
+ const renameCandidates = {};
404
+ // Gather possible rename candidates by comparing each added and removed index based on semantics.
405
+ for (const addedIndex of Object.values(tableDifferences.addedIndexes)) {
406
+ for (const removedIndex of Object.values(tableDifferences.removedIndexes)) {
407
+ if (this.diffIndex(addedIndex, removedIndex)) {
408
+ continue;
409
+ }
410
+ renameCandidates[addedIndex.keyName] = renameCandidates[addedIndex.keyName] ?? [];
411
+ renameCandidates[addedIndex.keyName].push([removedIndex, addedIndex]);
412
+ }
413
+ }
414
+ for (const candidateIndexes of Object.values(renameCandidates)) {
415
+ // If the current rename candidate contains exactly one semantically equal index, we can safely rename it.
416
+ // Otherwise it is unclear if a rename action is really intended, therefore we let those ambiguous indexes be added/dropped.
417
+ if (candidateIndexes.length !== 1) {
418
+ continue;
419
+ }
420
+ const [removedIndex, addedIndex] = candidateIndexes[0];
421
+ const removedIndexName = removedIndex.keyName;
422
+ const addedIndexName = addedIndex.keyName;
423
+ if (tableDifferences.renamedIndexes[removedIndexName]) {
424
+ continue;
425
+ }
426
+ tableDifferences.renamedIndexes[removedIndexName] = addedIndex;
427
+ delete tableDifferences.addedIndexes[addedIndexName];
428
+ delete tableDifferences.removedIndexes[removedIndexName];
429
+ this.log(`renamed index detected in table ${tableDifferences.name}`, {
430
+ old: removedIndexName,
431
+ new: addedIndexName,
432
+ });
433
+ }
434
+ }
435
+ diffForeignKey(key1, key2, tableDifferences) {
436
+ if (key1.columnNames.join('~').toLowerCase() !== key2.columnNames.join('~').toLowerCase()) {
437
+ return true;
438
+ }
439
+ if (key1.referencedColumnNames.join('~').toLowerCase() !== key2.referencedColumnNames.join('~').toLowerCase()) {
440
+ return true;
441
+ }
442
+ if (key1.constraintName !== key2.constraintName) {
443
+ return true;
444
+ }
445
+ if (key1.referencedTableName !== key2.referencedTableName) {
446
+ return true;
447
+ }
448
+ if (key1.deferMode !== key2.deferMode) {
449
+ return true;
450
+ }
451
+ if (key1.localTableName === key1.referencedTableName && !this.#platform.supportsMultipleCascadePaths()) {
452
+ return false;
453
+ }
454
+ if (key1.columnNames.some(col => tableDifferences.changedColumns[col]?.changedProperties.has('type'))) {
455
+ return true;
456
+ }
457
+ const defaultRule = ['restrict', 'no action'];
458
+ const rule = (key, method) => {
459
+ return (key[method] ?? defaultRule[0]).toLowerCase().replace(defaultRule[1], defaultRule[0]).replace(/"/g, '');
460
+ };
461
+ const compare = method => rule(key1, method) === rule(key2, method);
462
+ // Skip updateRule comparison for platforms that don't support ON UPDATE (e.g., Oracle)
463
+ const updateRuleDiffers = this.#platform.supportsOnUpdate() && !compare('updateRule');
464
+ return updateRuleDiffers || !compare('deleteRule');
465
+ }
466
+ /**
467
+ * Returns the difference between the columns
468
+ */
469
+ diffColumn(fromColumn, toColumn, fromTable, logging) {
470
+ const changedProperties = new Set();
471
+ const fromProp = this.mapColumnToProperty({ ...fromColumn, autoincrement: false });
472
+ const toProp = this.mapColumnToProperty({ ...toColumn, autoincrement: false });
473
+ const fromColumnType = this.#platform.normalizeColumnType(
474
+ fromColumn.mappedType.getColumnType(fromProp, this.#platform).toLowerCase(),
475
+ fromProp,
476
+ );
477
+ const fromNativeEnum =
478
+ fromTable.nativeEnums[fromColumnType] ??
479
+ Object.values(fromTable.nativeEnums).find(e => e.name === fromColumnType && e.schema !== '*');
480
+ let toColumnType = this.#platform.normalizeColumnType(
481
+ toColumn.mappedType.getColumnType(toProp, this.#platform).toLowerCase(),
482
+ toProp,
483
+ );
484
+ const log = (msg, params) => {
485
+ if (logging) {
486
+ const copy = Utils.copy(params);
487
+ Utils.dropUndefinedProperties(copy);
488
+ this.log(msg, copy);
489
+ }
490
+ };
491
+ if (
492
+ fromColumnType !== toColumnType &&
493
+ (!fromNativeEnum || `${fromNativeEnum.schema}.${fromNativeEnum.name}` !== toColumnType) &&
494
+ !(fromColumn.ignoreSchemaChanges?.includes('type') || toColumn.ignoreSchemaChanges?.includes('type')) &&
495
+ !fromColumn.generated &&
496
+ !toColumn.generated
497
+ ) {
498
+ if (
499
+ !toColumnType.includes('.') &&
500
+ fromTable.schema &&
501
+ fromTable.schema !== this.#platform.getDefaultSchemaName()
502
+ ) {
503
+ toColumnType = `${fromTable.schema}.${toColumnType}`;
504
+ }
505
+ if (fromColumnType !== toColumnType) {
506
+ log(`'type' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumnType, toColumnType });
507
+ changedProperties.add('type');
508
+ }
509
+ }
510
+ if (!!fromColumn.nullable !== !!toColumn.nullable && !fromColumn.generated && !toColumn.generated) {
511
+ log(`'nullable' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
512
+ changedProperties.add('nullable');
513
+ }
514
+ if (this.diffExpression(fromColumn.generated, toColumn.generated)) {
515
+ log(`'generated' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
516
+ changedProperties.add('generated');
517
+ }
518
+ if (!!fromColumn.autoincrement !== !!toColumn.autoincrement) {
519
+ log(`'autoincrement' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
520
+ changedProperties.add('autoincrement');
521
+ }
522
+ if (!!fromColumn.unsigned !== !!toColumn.unsigned && this.#platform.supportsUnsigned()) {
523
+ log(`'unsigned' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
524
+ changedProperties.add('unsigned');
525
+ }
526
+ if (
527
+ !(fromColumn.ignoreSchemaChanges?.includes('default') || toColumn.ignoreSchemaChanges?.includes('default')) &&
528
+ !this.hasSameDefaultValue(fromColumn, toColumn)
529
+ ) {
530
+ log(`'default' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
531
+ changedProperties.add('default');
532
+ }
533
+ if (this.diffComment(fromColumn.comment, toColumn.comment)) {
534
+ log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
535
+ changedProperties.add('comment');
536
+ }
537
+ if (
538
+ !(fromColumn.mappedType instanceof ArrayType) &&
539
+ !(toColumn.mappedType instanceof ArrayType) &&
540
+ this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)
541
+ ) {
542
+ log(`'enumItems' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
543
+ changedProperties.add('enumItems');
544
+ }
545
+ if (
546
+ (fromColumn.extra || '').toLowerCase() !== (toColumn.extra || '').toLowerCase() &&
547
+ !(fromColumn.ignoreSchemaChanges?.includes('extra') || toColumn.ignoreSchemaChanges?.includes('extra'))
548
+ ) {
549
+ log(`'extra' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
550
+ changedProperties.add('extra');
551
+ }
552
+ return changedProperties;
553
+ }
554
+ diffEnumItems(items1 = [], items2 = []) {
555
+ return items1.length !== items2.length || items1.some((v, i) => v !== items2[i]);
556
+ }
557
+ diffComment(comment1, comment2) {
558
+ // A null value and an empty string are actually equal for a comment so they should not trigger a change.
559
+ // eslint-disable-next-line eqeqeq
560
+ return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
561
+ }
562
+ /**
563
+ * Finds the difference between the indexes index1 and index2.
564
+ * Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
565
+ */
566
+ diffIndex(index1, index2) {
567
+ // if one of them is a custom expression or full text index, compare only by name
568
+ if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
569
+ return index1.keyName !== index2.keyName;
570
+ }
571
+ return !this.isIndexFulfilledBy(index1, index2) || !this.isIndexFulfilledBy(index2, index1);
572
+ }
573
+ /**
574
+ * Checks if the other index already fulfills all the indexing and constraint needs of the current one.
575
+ */
576
+ isIndexFulfilledBy(index1, index2) {
577
+ // allow the other index to be equally large only. It being larger is an option but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
578
+ if (index1.columnNames.length !== index2.columnNames.length) {
579
+ return false;
580
+ }
581
+ function spansColumns() {
582
+ for (let i = 0; i < index1.columnNames.length; i++) {
583
+ if (index1.columnNames[i] === index2.columnNames[i]) {
584
+ continue;
585
+ }
586
+ return false;
587
+ }
588
+ return true;
589
+ }
590
+ // Check if columns are the same, and even in the same order
591
+ if (!spansColumns()) {
592
+ return false;
593
+ }
594
+ // Compare advanced column options (sort order, nulls, length, collation)
595
+ if (!this.compareIndexColumns(index1, index2)) {
596
+ return false;
597
+ }
598
+ // Compare INCLUDE columns for covering indexes
599
+ if (!this.compareArrays(index1.include, index2.include)) {
600
+ return false;
601
+ }
602
+ // Compare fill factor
603
+ if (index1.fillFactor !== index2.fillFactor) {
604
+ return false;
605
+ }
606
+ // Compare invisible flag
607
+ if (!!index1.invisible !== !!index2.invisible) {
608
+ return false;
609
+ }
610
+ // Compare disabled flag
611
+ if (!!index1.disabled !== !!index2.disabled) {
612
+ return false;
613
+ }
614
+ // Compare clustered flag
615
+ if (!!index1.clustered !== !!index2.clustered) {
616
+ return false;
617
+ }
618
+ if (!index1.unique && !index1.primary) {
619
+ // this is a special case: If the current key is neither primary or unique, any unique or
620
+ // primary key will always have the same effect for the index and there cannot be any constraint
621
+ // overlaps. This means a primary or unique index can always fulfill the requirements of just an
622
+ // index that has no constraints.
623
+ return true;
624
+ }
625
+ if (this.#platform.supportsDeferredUniqueConstraints() && index1.deferMode !== index2.deferMode) {
626
+ return false;
627
+ }
628
+ return index1.primary === index2.primary && index1.unique === index2.unique;
629
+ }
630
+ /**
631
+ * Compare advanced column options between two indexes.
632
+ */
633
+ compareIndexColumns(index1, index2) {
634
+ const cols1 = index1.columns ?? [];
635
+ const cols2 = index2.columns ?? [];
636
+ // If neither has column options, they match
637
+ if (cols1.length === 0 && cols2.length === 0) {
638
+ return true;
639
+ }
640
+ // If only one has column options, they don't match
641
+ if (cols1.length !== cols2.length) {
642
+ return false;
643
+ }
644
+ // Compare each column's options
645
+ // Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
646
+ // and the columns array is derived from those same column names
647
+ for (let i = 0; i < cols1.length; i++) {
648
+ const c1 = cols1[i];
649
+ const c2 = cols2[i];
650
+ const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
651
+ const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
652
+ if (sort1 !== sort2) {
653
+ return false;
654
+ }
655
+ const defaultNulls = s => (s === 'DESC' ? 'FIRST' : 'LAST');
656
+ const nulls1 = c1.nulls?.toUpperCase() ?? defaultNulls(sort1);
657
+ const nulls2 = c2.nulls?.toUpperCase() ?? defaultNulls(sort2);
658
+ if (nulls1 !== nulls2) {
659
+ return false;
660
+ }
661
+ if (c1.length !== c2.length) {
662
+ return false;
663
+ }
664
+ if (c1.collation !== c2.collation) {
665
+ return false;
666
+ }
667
+ }
668
+ return true;
669
+ }
670
+ /**
671
+ * Compare two arrays for equality (order matters).
672
+ */
673
+ compareArrays(arr1, arr2) {
674
+ if (!arr1 && !arr2) {
675
+ return true;
676
+ }
677
+ if (!arr1 || !arr2 || arr1.length !== arr2.length) {
678
+ return false;
679
+ }
680
+ return arr1.every((val, i) => val === arr2[i]);
681
+ }
682
+ diffExpression(expr1, expr2) {
683
+ // expressions like check constraints might be normalized by the driver,
684
+ // e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
685
+ const simplify = str => {
686
+ return (
687
+ str
688
+ ?.replace(/_\w+'(.*?)'/g, '$1')
689
+ .replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
690
+ // MySQL normalizes count(*) to count(0)
691
+ .replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
692
+ // Remove quotes first so we can process identifiers
693
+ .replace(/['"`]/g, '')
694
+ // MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
695
+ // Strip these prefixes - match word.word patterns and keep only the last part
696
+ .replace(/\b\w+\.(\w+)/g, '$1')
697
+ // Normalize JOIN syntax: inner join -> join (equivalent in SQL)
698
+ .replace(/\binner\s+join\b/gi, 'join')
699
+ // Remove redundant column aliases like `title AS title` -> `title`
700
+ .replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
701
+ // Remove AS keyword (optional in SQL, MySQL may add/remove it)
702
+ .replace(/\bas\b/gi, '')
703
+ // Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
704
+ .replace(/[()\n[\]*]|::\w+| +/g, '')
705
+ .replace(/anyarray\[(.*)]/gi, '$1')
706
+ .toLowerCase()
707
+ // PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
708
+ // After removing AS and whitespace, this results in duplicate adjacent words
709
+ // Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
710
+ // Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
711
+ .replace(/(\w{3,})\1/g, '$1')
712
+ // Remove trailing semicolon (PostgreSQL adds it to view definitions)
713
+ .replace(/;$/, '')
714
+ );
715
+ };
716
+ return simplify(expr1) !== simplify(expr2);
717
+ }
718
+ /**
719
+ * Compares two view expressions, with special handling for SELECT *.
720
+ * Databases like PostgreSQL and MySQL expand `SELECT *` to explicit column names
721
+ * in their stored view definitions, which makes diffExpression always detect changes.
722
+ * When SELECT * is present, we strip the first SELECT...FROM column list from both
723
+ * sides and compare only the structural parts (FROM clause onwards).
724
+ * Note: this means changes *within* subqueries in the SELECT list of a SELECT * view
725
+ * may not be detected — an acceptable tradeoff since SELECT * views are inherently
726
+ * column-list-agnostic.
727
+ * @see https://github.com/mikro-orm/mikro-orm/issues/7308
728
+ */
729
+ diffViewExpression(fromDef, toDef) {
730
+ if (!this.diffExpression(fromDef, toDef)) {
731
+ return false;
732
+ }
733
+ // If either expression uses SELECT *, the diff may be due to * expansion
734
+ if (/\bselect\s+\*/i.test(fromDef) || /\bselect\s+\*/i.test(toDef)) {
735
+ const stripColumns = s => s.replace(/\bselect\b[\s\S]*?\bfrom\b/i, 'select from');
736
+ return this.diffExpression(stripColumns(fromDef), stripColumns(toDef));
737
+ }
738
+ return true;
739
+ }
740
+ parseJsonDefault(defaultValue) {
741
+ /* v8 ignore next */
742
+ if (!defaultValue) {
743
+ return null;
744
+ }
745
+ const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
746
+ return parseJsonSafe(val);
747
+ }
748
+ hasSameDefaultValue(from, to) {
749
+ if (
750
+ from.default == null ||
751
+ from.default.toString().toLowerCase() === 'null' ||
752
+ from.default.toString().startsWith('nextval(')
753
+ ) {
754
+ return to.default == null || to.default.toLowerCase() === 'null';
755
+ }
756
+ if (to.mappedType instanceof BooleanType) {
757
+ const defaultValueFrom = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + from.default);
758
+ const defaultValueTo = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + to.default);
759
+ return defaultValueFrom === defaultValueTo;
760
+ }
761
+ if (to.mappedType instanceof JsonType) {
762
+ const defaultValueFrom = this.parseJsonDefault(from.default);
763
+ const defaultValueTo = this.parseJsonDefault(to.default);
764
+ return Utils.equals(defaultValueFrom, defaultValueTo);
765
+ }
766
+ if (to.mappedType instanceof DateTimeType && from.default && to.default) {
767
+ // normalize now/current_timestamp defaults, also remove `()` from the end of default expression
768
+ const defaultValueFrom = from.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
769
+ const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
770
+ return defaultValueFrom === defaultValueTo;
771
+ }
772
+ if (from.default && to.default) {
773
+ return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
774
+ }
775
+ if (['', this.#helper.getDefaultEmptyString()].includes(to.default) && from.default != null) {
776
+ return ['', this.#helper.getDefaultEmptyString()].includes(from.default.toString());
777
+ }
778
+ // eslint-disable-next-line eqeqeq
779
+ return from.default == to.default; // == intentionally
780
+ }
781
+ mapColumnToProperty(column) {
782
+ const length = /\w+\((\d+)\)/.exec(column.type);
783
+ const match = /\w+\((\d+), ?(\d+)\)/.exec(column.type);
784
+ return {
785
+ fieldNames: [column.name],
786
+ columnTypes: [column.type],
787
+ items: column.enumItems,
788
+ ...column,
789
+ length: length ? +length[1] : column.length,
790
+ precision: match ? +match[1] : column.precision,
791
+ scale: match ? +match[2] : column.scale,
792
+ };
793
+ }
794
+ log(message, params) {
795
+ if (params) {
796
+ message += ' ' + inspect(params);
773
797
  }
798
+ this.#logger.log('schema', message);
799
+ }
774
800
  }