@mikro-orm/sql 7.0.4 → 7.0.5-dev.1

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