Initial commit - Event Planner application
This commit is contained in:
753
node_modules/@mikro-orm/sql/schema/SchemaComparator.js
generated
vendored
Normal file
753
node_modules/@mikro-orm/sql/schema/SchemaComparator.js
generated
vendored
Normal file
@@ -0,0 +1,753 @@
|
||||
import { ArrayType, BooleanType, DateTimeType, inspect, JsonType, parseJsonSafe, Utils } from '@mikro-orm/core';
|
||||
/**
|
||||
* Compares two Schemas and return an instance of SchemaDifference.
|
||||
*/
|
||||
export class SchemaComparator {
|
||||
#helper;
|
||||
#logger;
|
||||
#platform;
|
||||
constructor(platform) {
|
||||
this.#platform = platform;
|
||||
this.#helper = this.#platform.getSchemaHelper();
|
||||
this.#logger = this.#platform.getConfig().getLogger();
|
||||
}
|
||||
/**
|
||||
* Returns a SchemaDifference object containing the differences between the schemas fromSchema and toSchema.
|
||||
*
|
||||
* The returned differences are returned in such a way that they contain the
|
||||
* operations to change the schema stored in fromSchema to the schema that is
|
||||
* stored in toSchema.
|
||||
*/
|
||||
compare(fromSchema, toSchema, inverseDiff) {
|
||||
const diff = {
|
||||
newTables: {},
|
||||
removedTables: {},
|
||||
changedTables: {},
|
||||
newViews: {},
|
||||
changedViews: {},
|
||||
removedViews: {},
|
||||
orphanedForeignKeys: [],
|
||||
newNativeEnums: [],
|
||||
removedNativeEnums: [],
|
||||
newNamespaces: new Set(),
|
||||
removedNamespaces: new Set(),
|
||||
fromSchema,
|
||||
};
|
||||
const foreignKeysToTable = {};
|
||||
for (const namespace of toSchema.getNamespaces()) {
|
||||
if (fromSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
|
||||
continue;
|
||||
}
|
||||
diff.newNamespaces.add(namespace);
|
||||
}
|
||||
for (const namespace of fromSchema.getNamespaces()) {
|
||||
if (toSchema.hasNamespace(namespace) || namespace === this.#platform.getDefaultSchemaName()) {
|
||||
continue;
|
||||
}
|
||||
diff.removedNamespaces.add(namespace);
|
||||
}
|
||||
for (const [key, nativeEnum] of Object.entries(toSchema.getNativeEnums())) {
|
||||
if (fromSchema.hasNativeEnum(key)) {
|
||||
continue;
|
||||
}
|
||||
if (nativeEnum.schema === '*' && fromSchema.hasNativeEnum(`${toSchema.name}.${key}`)) {
|
||||
continue;
|
||||
}
|
||||
diff.newNativeEnums.push(nativeEnum);
|
||||
}
|
||||
for (const [key, nativeEnum] of Object.entries(fromSchema.getNativeEnums())) {
|
||||
if (toSchema.hasNativeEnum(key)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
key.startsWith(`${fromSchema.name}.`) &&
|
||||
(fromSchema.name !== toSchema.name ||
|
||||
toSchema.getNativeEnum(key.substring(fromSchema.name.length + 1))?.schema === '*')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
diff.removedNativeEnums.push(nativeEnum);
|
||||
}
|
||||
for (const table of toSchema.getTables()) {
|
||||
const tableName = table.getShortestName(false);
|
||||
if (!fromSchema.hasTable(tableName)) {
|
||||
diff.newTables[tableName] = toSchema.getTable(tableName);
|
||||
} else {
|
||||
const tableDifferences = this.diffTable(
|
||||
fromSchema.getTable(tableName),
|
||||
toSchema.getTable(tableName),
|
||||
inverseDiff?.changedTables[tableName],
|
||||
);
|
||||
if (tableDifferences !== false) {
|
||||
diff.changedTables[tableName] = tableDifferences;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if there are tables removed
|
||||
for (let table of fromSchema.getTables()) {
|
||||
const tableName = table.getShortestName();
|
||||
table = fromSchema.getTable(tableName);
|
||||
if (!toSchema.hasTable(tableName)) {
|
||||
diff.removedTables[tableName] = table;
|
||||
}
|
||||
// also remember all foreign keys that point to a specific table
|
||||
for (const foreignKey of Object.values(table.getForeignKeys())) {
|
||||
if (!foreignKeysToTable[foreignKey.referencedTableName]) {
|
||||
foreignKeysToTable[foreignKey.referencedTableName] = [];
|
||||
}
|
||||
foreignKeysToTable[foreignKey.referencedTableName].push(foreignKey);
|
||||
}
|
||||
}
|
||||
for (const table of Object.values(diff.removedTables)) {
|
||||
const tableName = (table.schema ? table.schema + '.' : '') + table.name;
|
||||
if (!foreignKeysToTable[tableName]) {
|
||||
continue;
|
||||
}
|
||||
diff.orphanedForeignKeys.push(...foreignKeysToTable[tableName]);
|
||||
// Deleting duplicated foreign keys present both on the orphanedForeignKey and the removedForeignKeys from changedTables.
|
||||
for (const foreignKey of foreignKeysToTable[tableName]) {
|
||||
const localTableName = foreignKey.localTableName;
|
||||
if (!diff.changedTables[localTableName]) {
|
||||
continue;
|
||||
}
|
||||
for (const [key, fk] of Object.entries(diff.changedTables[localTableName].removedForeignKeys)) {
|
||||
// We check if the key is from the removed table, if not we skip.
|
||||
if (tableName !== fk.referencedTableName) {
|
||||
continue;
|
||||
}
|
||||
delete diff.changedTables[localTableName].removedForeignKeys[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Compare views
|
||||
for (const toView of toSchema.getViews()) {
|
||||
const viewName = toView.schema ? `${toView.schema}.${toView.name}` : toView.name;
|
||||
if (!fromSchema.hasView(toView.name) && !fromSchema.hasView(viewName)) {
|
||||
diff.newViews[viewName] = toView;
|
||||
this.log(`view ${viewName} added`);
|
||||
} else {
|
||||
const fromView = fromSchema.getView(toView.name) ?? fromSchema.getView(viewName);
|
||||
if (fromView && this.diffExpression(fromView.definition, toView.definition)) {
|
||||
diff.changedViews[viewName] = { from: fromView, to: toView };
|
||||
this.log(`view ${viewName} changed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for removed views
|
||||
for (const fromView of fromSchema.getViews()) {
|
||||
const viewName = fromView.schema ? `${fromView.schema}.${fromView.name}` : fromView.name;
|
||||
if (!toSchema.hasView(fromView.name) && !toSchema.hasView(viewName)) {
|
||||
diff.removedViews[viewName] = fromView;
|
||||
this.log(`view ${viewName} removed`);
|
||||
}
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
/**
|
||||
* Returns the difference between the tables fromTable and toTable.
|
||||
* If there are no differences this method returns the boolean false.
|
||||
*/
|
||||
diffTable(fromTable, toTable, inverseTableDiff) {
|
||||
let changes = 0;
|
||||
const tableDifferences = {
|
||||
name: fromTable.getShortestName(),
|
||||
addedColumns: {},
|
||||
addedForeignKeys: {},
|
||||
addedIndexes: {},
|
||||
addedChecks: {},
|
||||
changedColumns: {},
|
||||
changedForeignKeys: {},
|
||||
changedIndexes: {},
|
||||
changedChecks: {},
|
||||
removedColumns: {},
|
||||
removedForeignKeys: {},
|
||||
removedIndexes: {},
|
||||
removedChecks: {},
|
||||
renamedColumns: {},
|
||||
renamedIndexes: {},
|
||||
fromTable,
|
||||
toTable,
|
||||
};
|
||||
if (this.diffComment(fromTable.comment, toTable.comment)) {
|
||||
tableDifferences.changedComment = toTable.comment;
|
||||
this.log(`table comment changed for ${tableDifferences.name}`, {
|
||||
fromTableComment: fromTable.comment,
|
||||
toTableComment: toTable.comment,
|
||||
});
|
||||
changes++;
|
||||
}
|
||||
const fromTableColumns = fromTable.getColumns();
|
||||
const toTableColumns = toTable.getColumns();
|
||||
// See if all the columns in "from" table exist in "to" table
|
||||
for (const column of toTableColumns) {
|
||||
if (fromTable.hasColumn(column.name)) {
|
||||
continue;
|
||||
}
|
||||
tableDifferences.addedColumns[column.name] = column;
|
||||
this.log(`column ${tableDifferences.name}.${column.name} of type ${column.type} added`);
|
||||
changes++;
|
||||
}
|
||||
/* See if there are any removed columns in "to" table */
|
||||
for (const column of fromTableColumns) {
|
||||
// See if column is removed in "to" table.
|
||||
if (!toTable.hasColumn(column.name)) {
|
||||
tableDifferences.removedColumns[column.name] = column;
|
||||
this.log(`column ${tableDifferences.name}.${column.name} removed`);
|
||||
changes++;
|
||||
continue;
|
||||
}
|
||||
// See if column has changed properties in "to" table.
|
||||
const changedProperties = this.diffColumn(column, toTable.getColumn(column.name), fromTable, true);
|
||||
if (changedProperties.size === 0) {
|
||||
continue;
|
||||
}
|
||||
if (changedProperties.size === 1 && changedProperties.has('generated')) {
|
||||
tableDifferences.addedColumns[column.name] = toTable.getColumn(column.name);
|
||||
tableDifferences.removedColumns[column.name] = column;
|
||||
changes++;
|
||||
continue;
|
||||
}
|
||||
tableDifferences.changedColumns[column.name] = {
|
||||
oldColumnName: column.name,
|
||||
fromColumn: column,
|
||||
column: toTable.getColumn(column.name),
|
||||
changedProperties,
|
||||
};
|
||||
this.log(`column ${tableDifferences.name}.${column.name} changed`, { changedProperties });
|
||||
changes++;
|
||||
}
|
||||
this.detectColumnRenamings(tableDifferences, inverseTableDiff);
|
||||
const fromTableIndexes = fromTable.getIndexes();
|
||||
const toTableIndexes = toTable.getIndexes();
|
||||
// See if all the indexes in "from" table exist in "to" table
|
||||
for (const index of Object.values(toTableIndexes)) {
|
||||
if ((index.primary && fromTableIndexes.find(i => i.primary)) || fromTable.hasIndex(index.keyName)) {
|
||||
continue;
|
||||
}
|
||||
tableDifferences.addedIndexes[index.keyName] = index;
|
||||
this.log(`index ${index.keyName} added to table ${tableDifferences.name}`, { index });
|
||||
changes++;
|
||||
}
|
||||
// See if there are any removed indexes in "to" table
|
||||
for (const index of fromTableIndexes) {
|
||||
// See if index is removed in "to" table.
|
||||
if ((index.primary && !toTable.hasPrimaryKey()) || (!index.primary && !toTable.hasIndex(index.keyName))) {
|
||||
tableDifferences.removedIndexes[index.keyName] = index;
|
||||
this.log(`index ${index.keyName} removed from table ${tableDifferences.name}`);
|
||||
changes++;
|
||||
continue;
|
||||
}
|
||||
// See if index has changed in "to" table.
|
||||
const toTableIndex = index.primary ? toTable.getPrimaryKey() : toTable.getIndex(index.keyName);
|
||||
if (!this.diffIndex(index, toTableIndex)) {
|
||||
continue;
|
||||
}
|
||||
tableDifferences.changedIndexes[index.keyName] = toTableIndex;
|
||||
this.log(`index ${index.keyName} changed in table ${tableDifferences.name}`, {
|
||||
fromTableIndex: index,
|
||||
toTableIndex,
|
||||
});
|
||||
changes++;
|
||||
}
|
||||
this.detectIndexRenamings(tableDifferences);
|
||||
const fromTableChecks = fromTable.getChecks();
|
||||
const toTableChecks = toTable.getChecks();
|
||||
// See if all the checks in "from" table exist in "to" table
|
||||
for (const check of toTableChecks) {
|
||||
if (fromTable.hasCheck(check.name)) {
|
||||
continue;
|
||||
}
|
||||
tableDifferences.addedChecks[check.name] = check;
|
||||
this.log(`check constraint ${check.name} added to table ${tableDifferences.name}`, { check });
|
||||
changes++;
|
||||
}
|
||||
// See if there are any removed checks in "to" table
|
||||
for (const check of fromTableChecks) {
|
||||
if (!toTable.hasCheck(check.name)) {
|
||||
tableDifferences.removedChecks[check.name] = check;
|
||||
this.log(`check constraint ${check.name} removed from table ${tableDifferences.name}`);
|
||||
changes++;
|
||||
continue;
|
||||
}
|
||||
// See if check has changed in "to" table
|
||||
const toTableCheck = toTable.getCheck(check.name);
|
||||
const toColumn = toTable.getColumn(check.columnName);
|
||||
const fromColumn = fromTable.getColumn(check.columnName);
|
||||
if (!this.diffExpression(check.expression, toTableCheck.expression)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
fromColumn?.enumItems &&
|
||||
toColumn?.enumItems &&
|
||||
!this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
this.log(`check constraint ${check.name} changed in table ${tableDifferences.name}`, {
|
||||
fromTableCheck: check,
|
||||
toTableCheck,
|
||||
});
|
||||
tableDifferences.changedChecks[check.name] = toTableCheck;
|
||||
changes++;
|
||||
}
|
||||
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
||||
const toForeignKeys = { ...toTable.getForeignKeys() };
|
||||
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
||||
for (const toConstraint of Object.values(toForeignKeys)) {
|
||||
if (!this.diffForeignKey(fromConstraint, toConstraint, tableDifferences)) {
|
||||
delete fromForeignKeys[fromConstraint.constraintName];
|
||||
delete toForeignKeys[toConstraint.constraintName];
|
||||
} else if (fromConstraint.constraintName.toLowerCase() === toConstraint.constraintName.toLowerCase()) {
|
||||
this.log(`FK constraint ${fromConstraint.constraintName} changed in table ${tableDifferences.name}`, {
|
||||
fromConstraint,
|
||||
toConstraint,
|
||||
});
|
||||
tableDifferences.changedForeignKeys[toConstraint.constraintName] = toConstraint;
|
||||
changes++;
|
||||
delete fromForeignKeys[fromConstraint.constraintName];
|
||||
delete toForeignKeys[toConstraint.constraintName];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
||||
tableDifferences.removedForeignKeys[fromConstraint.constraintName] = fromConstraint;
|
||||
this.log(`FK constraint ${fromConstraint.constraintName} removed from table ${tableDifferences.name}`);
|
||||
changes++;
|
||||
}
|
||||
for (const toConstraint of Object.values(toForeignKeys)) {
|
||||
tableDifferences.addedForeignKeys[toConstraint.constraintName] = toConstraint;
|
||||
this.log(`FK constraint ${toConstraint.constraintName} added to table ${tableDifferences.name}`, {
|
||||
constraint: toConstraint,
|
||||
});
|
||||
changes++;
|
||||
}
|
||||
return changes ? tableDifferences : false;
|
||||
}
|
||||
/**
|
||||
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
|
||||
* however ambiguities between different possibilities should not lead to renaming at all.
|
||||
*/
|
||||
detectColumnRenamings(tableDifferences, inverseTableDiff) {
|
||||
const renameCandidates = {};
|
||||
const oldFKs = Object.values(tableDifferences.fromTable.getForeignKeys());
|
||||
const newFKs = Object.values(tableDifferences.toTable.getForeignKeys());
|
||||
for (const addedColumn of Object.values(tableDifferences.addedColumns)) {
|
||||
for (const removedColumn of Object.values(tableDifferences.removedColumns)) {
|
||||
const diff = this.diffColumn(addedColumn, removedColumn, tableDifferences.fromTable);
|
||||
if (diff.size !== 0) {
|
||||
continue;
|
||||
}
|
||||
const wasFK = oldFKs.some(fk => fk.columnNames.includes(removedColumn.name));
|
||||
const isFK = newFKs.some(fk => fk.columnNames.includes(addedColumn.name));
|
||||
if (wasFK !== isFK) {
|
||||
continue;
|
||||
}
|
||||
const renamedColumn = inverseTableDiff?.renamedColumns[addedColumn.name];
|
||||
if (renamedColumn && renamedColumn?.name !== removedColumn.name) {
|
||||
continue;
|
||||
}
|
||||
renameCandidates[addedColumn.name] = renameCandidates[addedColumn.name] ?? [];
|
||||
renameCandidates[addedColumn.name].push([removedColumn, addedColumn]);
|
||||
}
|
||||
}
|
||||
for (const candidateColumns of Object.values(renameCandidates)) {
|
||||
if (candidateColumns.length !== 1) {
|
||||
continue;
|
||||
}
|
||||
const [removedColumn, addedColumn] = candidateColumns[0];
|
||||
const removedColumnName = removedColumn.name;
|
||||
const addedColumnName = addedColumn.name;
|
||||
/* v8 ignore next */
|
||||
if (tableDifferences.renamedColumns[removedColumnName]) {
|
||||
continue;
|
||||
}
|
||||
tableDifferences.renamedColumns[removedColumnName] = addedColumn;
|
||||
delete tableDifferences.addedColumns[addedColumnName];
|
||||
delete tableDifferences.removedColumns[removedColumnName];
|
||||
this.log(`renamed column detected in table ${tableDifferences.name}`, {
|
||||
old: removedColumnName,
|
||||
new: addedColumnName,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
|
||||
* however ambiguities between different possibilities should not lead to renaming at all.
|
||||
*/
|
||||
detectIndexRenamings(tableDifferences) {
|
||||
const renameCandidates = {};
|
||||
// Gather possible rename candidates by comparing each added and removed index based on semantics.
|
||||
for (const addedIndex of Object.values(tableDifferences.addedIndexes)) {
|
||||
for (const removedIndex of Object.values(tableDifferences.removedIndexes)) {
|
||||
if (this.diffIndex(addedIndex, removedIndex)) {
|
||||
continue;
|
||||
}
|
||||
renameCandidates[addedIndex.keyName] = renameCandidates[addedIndex.keyName] ?? [];
|
||||
renameCandidates[addedIndex.keyName].push([removedIndex, addedIndex]);
|
||||
}
|
||||
}
|
||||
for (const candidateIndexes of Object.values(renameCandidates)) {
|
||||
// If the current rename candidate contains exactly one semantically equal index, we can safely rename it.
|
||||
// Otherwise it is unclear if a rename action is really intended, therefore we let those ambiguous indexes be added/dropped.
|
||||
if (candidateIndexes.length !== 1) {
|
||||
continue;
|
||||
}
|
||||
const [removedIndex, addedIndex] = candidateIndexes[0];
|
||||
const removedIndexName = removedIndex.keyName;
|
||||
const addedIndexName = addedIndex.keyName;
|
||||
if (tableDifferences.renamedIndexes[removedIndexName]) {
|
||||
continue;
|
||||
}
|
||||
tableDifferences.renamedIndexes[removedIndexName] = addedIndex;
|
||||
delete tableDifferences.addedIndexes[addedIndexName];
|
||||
delete tableDifferences.removedIndexes[removedIndexName];
|
||||
this.log(`renamed index detected in table ${tableDifferences.name}`, {
|
||||
old: removedIndexName,
|
||||
new: addedIndexName,
|
||||
});
|
||||
}
|
||||
}
|
||||
diffForeignKey(key1, key2, tableDifferences) {
|
||||
if (key1.columnNames.join('~').toLowerCase() !== key2.columnNames.join('~').toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
if (key1.referencedColumnNames.join('~').toLowerCase() !== key2.referencedColumnNames.join('~').toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
if (key1.constraintName !== key2.constraintName) {
|
||||
return true;
|
||||
}
|
||||
if (key1.referencedTableName !== key2.referencedTableName) {
|
||||
return true;
|
||||
}
|
||||
if (key1.deferMode !== key2.deferMode) {
|
||||
return true;
|
||||
}
|
||||
if (key1.localTableName === key1.referencedTableName && !this.#platform.supportsMultipleCascadePaths()) {
|
||||
return false;
|
||||
}
|
||||
if (key1.columnNames.some(col => tableDifferences.changedColumns[col]?.changedProperties.has('type'))) {
|
||||
return true;
|
||||
}
|
||||
const defaultRule = ['restrict', 'no action'];
|
||||
const rule = (key, method) => {
|
||||
return (key[method] ?? defaultRule[0]).toLowerCase().replace(defaultRule[1], defaultRule[0]).replace(/"/g, '');
|
||||
};
|
||||
const compare = method => rule(key1, method) === rule(key2, method);
|
||||
// Skip updateRule comparison for platforms that don't support ON UPDATE (e.g., Oracle)
|
||||
const updateRuleDiffers = this.#platform.supportsOnUpdate() && !compare('updateRule');
|
||||
return updateRuleDiffers || !compare('deleteRule');
|
||||
}
|
||||
/**
|
||||
* Returns the difference between the columns
|
||||
*/
|
||||
diffColumn(fromColumn, toColumn, fromTable, logging) {
|
||||
const changedProperties = new Set();
|
||||
const fromProp = this.mapColumnToProperty({ ...fromColumn, autoincrement: false });
|
||||
const toProp = this.mapColumnToProperty({ ...toColumn, autoincrement: false });
|
||||
const fromColumnType = this.#platform.normalizeColumnType(
|
||||
fromColumn.mappedType.getColumnType(fromProp, this.#platform).toLowerCase(),
|
||||
fromProp,
|
||||
);
|
||||
const fromNativeEnum =
|
||||
fromTable.nativeEnums[fromColumnType] ??
|
||||
Object.values(fromTable.nativeEnums).find(e => e.name === fromColumnType && e.schema !== '*');
|
||||
let toColumnType = this.#platform.normalizeColumnType(
|
||||
toColumn.mappedType.getColumnType(toProp, this.#platform).toLowerCase(),
|
||||
toProp,
|
||||
);
|
||||
const log = (msg, params) => {
|
||||
if (logging) {
|
||||
const copy = Utils.copy(params);
|
||||
Utils.dropUndefinedProperties(copy);
|
||||
this.log(msg, copy);
|
||||
}
|
||||
};
|
||||
if (
|
||||
fromColumnType !== toColumnType &&
|
||||
(!fromNativeEnum || `${fromNativeEnum.schema}.${fromNativeEnum.name}` !== toColumnType) &&
|
||||
!(fromColumn.ignoreSchemaChanges?.includes('type') || toColumn.ignoreSchemaChanges?.includes('type')) &&
|
||||
!fromColumn.generated &&
|
||||
!toColumn.generated
|
||||
) {
|
||||
if (
|
||||
!toColumnType.includes('.') &&
|
||||
fromTable.schema &&
|
||||
fromTable.schema !== this.#platform.getDefaultSchemaName()
|
||||
) {
|
||||
toColumnType = `${fromTable.schema}.${toColumnType}`;
|
||||
}
|
||||
if (fromColumnType !== toColumnType) {
|
||||
log(`'type' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumnType, toColumnType });
|
||||
changedProperties.add('type');
|
||||
}
|
||||
}
|
||||
if (!!fromColumn.nullable !== !!toColumn.nullable && !fromColumn.generated && !toColumn.generated) {
|
||||
log(`'nullable' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('nullable');
|
||||
}
|
||||
if (this.diffExpression(fromColumn.generated, toColumn.generated)) {
|
||||
log(`'generated' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('generated');
|
||||
}
|
||||
if (!!fromColumn.autoincrement !== !!toColumn.autoincrement) {
|
||||
log(`'autoincrement' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('autoincrement');
|
||||
}
|
||||
if (!!fromColumn.unsigned !== !!toColumn.unsigned && this.#platform.supportsUnsigned()) {
|
||||
log(`'unsigned' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('unsigned');
|
||||
}
|
||||
if (
|
||||
!(fromColumn.ignoreSchemaChanges?.includes('default') || toColumn.ignoreSchemaChanges?.includes('default')) &&
|
||||
!this.hasSameDefaultValue(fromColumn, toColumn)
|
||||
) {
|
||||
log(`'default' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('default');
|
||||
}
|
||||
if (this.diffComment(fromColumn.comment, toColumn.comment)) {
|
||||
log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('comment');
|
||||
}
|
||||
if (
|
||||
!(fromColumn.mappedType instanceof ArrayType) &&
|
||||
!(toColumn.mappedType instanceof ArrayType) &&
|
||||
this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)
|
||||
) {
|
||||
log(`'enumItems' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('enumItems');
|
||||
}
|
||||
if (
|
||||
(fromColumn.extra || '').toLowerCase() !== (toColumn.extra || '').toLowerCase() &&
|
||||
!(fromColumn.ignoreSchemaChanges?.includes('extra') || toColumn.ignoreSchemaChanges?.includes('extra'))
|
||||
) {
|
||||
log(`'extra' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
||||
changedProperties.add('extra');
|
||||
}
|
||||
return changedProperties;
|
||||
}
|
||||
diffEnumItems(items1 = [], items2 = []) {
|
||||
return items1.length !== items2.length || items1.some((v, i) => v !== items2[i]);
|
||||
}
|
||||
diffComment(comment1, comment2) {
|
||||
// A null value and an empty string are actually equal for a comment so they should not trigger a change.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
|
||||
}
|
||||
/**
|
||||
* Finds the difference between the indexes index1 and index2.
|
||||
* Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
|
||||
*/
|
||||
diffIndex(index1, index2) {
|
||||
// if one of them is a custom expression or full text index, compare only by name
|
||||
if (index1.expression || index2.expression || index1.type === 'fulltext' || index2.type === 'fulltext') {
|
||||
return index1.keyName !== index2.keyName;
|
||||
}
|
||||
return !this.isIndexFulfilledBy(index1, index2) || !this.isIndexFulfilledBy(index2, index1);
|
||||
}
|
||||
/**
|
||||
* Checks if the other index already fulfills all the indexing and constraint needs of the current one.
|
||||
*/
|
||||
isIndexFulfilledBy(index1, index2) {
|
||||
// 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)
|
||||
if (index1.columnNames.length !== index2.columnNames.length) {
|
||||
return false;
|
||||
}
|
||||
function spansColumns() {
|
||||
for (let i = 0; i < index1.columnNames.length; i++) {
|
||||
if (index1.columnNames[i] === index2.columnNames[i]) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Check if columns are the same, and even in the same order
|
||||
if (!spansColumns()) {
|
||||
return false;
|
||||
}
|
||||
// Compare advanced column options (sort order, nulls, length, collation)
|
||||
if (!this.compareIndexColumns(index1, index2)) {
|
||||
return false;
|
||||
}
|
||||
// Compare INCLUDE columns for covering indexes
|
||||
if (!this.compareArrays(index1.include, index2.include)) {
|
||||
return false;
|
||||
}
|
||||
// Compare fill factor
|
||||
if (index1.fillFactor !== index2.fillFactor) {
|
||||
return false;
|
||||
}
|
||||
// Compare invisible flag
|
||||
if (!!index1.invisible !== !!index2.invisible) {
|
||||
return false;
|
||||
}
|
||||
// Compare disabled flag
|
||||
if (!!index1.disabled !== !!index2.disabled) {
|
||||
return false;
|
||||
}
|
||||
// Compare clustered flag
|
||||
if (!!index1.clustered !== !!index2.clustered) {
|
||||
return false;
|
||||
}
|
||||
if (!index1.unique && !index1.primary) {
|
||||
// this is a special case: If the current key is neither primary or unique, any unique or
|
||||
// primary key will always have the same effect for the index and there cannot be any constraint
|
||||
// overlaps. This means a primary or unique index can always fulfill the requirements of just an
|
||||
// index that has no constraints.
|
||||
return true;
|
||||
}
|
||||
if (this.#platform.supportsDeferredUniqueConstraints() && index1.deferMode !== index2.deferMode) {
|
||||
return false;
|
||||
}
|
||||
return index1.primary === index2.primary && index1.unique === index2.unique;
|
||||
}
|
||||
/**
|
||||
* Compare advanced column options between two indexes.
|
||||
*/
|
||||
compareIndexColumns(index1, index2) {
|
||||
const cols1 = index1.columns ?? [];
|
||||
const cols2 = index2.columns ?? [];
|
||||
// If neither has column options, they match
|
||||
if (cols1.length === 0 && cols2.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// If only one has column options, they don't match
|
||||
if (cols1.length !== cols2.length) {
|
||||
return false;
|
||||
}
|
||||
// Compare each column's options
|
||||
// Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
|
||||
// and the columns array is derived from those same column names
|
||||
for (let i = 0; i < cols1.length; i++) {
|
||||
const c1 = cols1[i];
|
||||
const c2 = cols2[i];
|
||||
const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
|
||||
const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
|
||||
if (sort1 !== sort2) {
|
||||
return false;
|
||||
}
|
||||
const defaultNulls = s => (s === 'DESC' ? 'FIRST' : 'LAST');
|
||||
const nulls1 = c1.nulls?.toUpperCase() ?? defaultNulls(sort1);
|
||||
const nulls2 = c2.nulls?.toUpperCase() ?? defaultNulls(sort2);
|
||||
if (nulls1 !== nulls2) {
|
||||
return false;
|
||||
}
|
||||
if (c1.length !== c2.length) {
|
||||
return false;
|
||||
}
|
||||
if (c1.collation !== c2.collation) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Compare two arrays for equality (order matters).
|
||||
*/
|
||||
compareArrays(arr1, arr2) {
|
||||
if (!arr1 && !arr2) {
|
||||
return true;
|
||||
}
|
||||
if (!arr1 || !arr2 || arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
return arr1.every((val, i) => val === arr2[i]);
|
||||
}
|
||||
diffExpression(expr1, expr2) {
|
||||
// expressions like check constraints might be normalized by the driver,
|
||||
// e.g. quotes might be added (https://github.com/mikro-orm/mikro-orm/issues/3827)
|
||||
const simplify = str => {
|
||||
return (
|
||||
str
|
||||
?.replace(/_\w+'(.*?)'/g, '$1')
|
||||
.replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
|
||||
// MySQL normalizes count(*) to count(0)
|
||||
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
|
||||
// Remove quotes first so we can process identifiers
|
||||
.replace(/['"`]/g, '')
|
||||
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
||||
// Strip these prefixes - match word.word patterns and keep only the last part
|
||||
.replace(/\b\w+\.(\w+)/g, '$1')
|
||||
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
|
||||
.replace(/\binner\s+join\b/gi, 'join')
|
||||
// Remove redundant column aliases like `title AS title` -> `title`
|
||||
.replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
|
||||
// Remove AS keyword (optional in SQL, MySQL may add/remove it)
|
||||
.replace(/\bas\b/gi, '')
|
||||
// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
|
||||
.replace(/[()\n[\]*]|::\w+| +/g, '')
|
||||
.replace(/anyarray\[(.*)]/gi, '$1')
|
||||
.toLowerCase()
|
||||
// PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
|
||||
// After removing AS and whitespace, this results in duplicate adjacent words
|
||||
// Remove these duplicates: "countcount" -> "count", "minmin" -> "min"
|
||||
// Use lookahead to match repeated patterns of 3+ chars (avoid false positives on short sequences)
|
||||
.replace(/(\w{3,})\1/g, '$1')
|
||||
// Remove trailing semicolon (PostgreSQL adds it to view definitions)
|
||||
.replace(/;$/, '')
|
||||
);
|
||||
};
|
||||
return simplify(expr1) !== simplify(expr2);
|
||||
}
|
||||
parseJsonDefault(defaultValue) {
|
||||
/* v8 ignore next */
|
||||
if (!defaultValue) {
|
||||
return null;
|
||||
}
|
||||
const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
|
||||
return parseJsonSafe(val);
|
||||
}
|
||||
hasSameDefaultValue(from, to) {
|
||||
if (
|
||||
from.default == null ||
|
||||
from.default.toString().toLowerCase() === 'null' ||
|
||||
from.default.toString().startsWith('nextval(')
|
||||
) {
|
||||
return to.default == null || to.default.toLowerCase() === 'null';
|
||||
}
|
||||
if (to.mappedType instanceof BooleanType) {
|
||||
const defaultValueFrom = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + from.default);
|
||||
const defaultValueTo = !['0', 'false', 'f', 'n', 'no', 'off'].includes('' + to.default);
|
||||
return defaultValueFrom === defaultValueTo;
|
||||
}
|
||||
if (to.mappedType instanceof JsonType) {
|
||||
const defaultValueFrom = this.parseJsonDefault(from.default);
|
||||
const defaultValueTo = this.parseJsonDefault(to.default);
|
||||
return Utils.equals(defaultValueFrom, defaultValueTo);
|
||||
}
|
||||
if (to.mappedType instanceof DateTimeType && from.default && to.default) {
|
||||
// normalize now/current_timestamp defaults, also remove `()` from the end of default expression
|
||||
const defaultValueFrom = from.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
|
||||
const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
|
||||
return defaultValueFrom === defaultValueTo;
|
||||
}
|
||||
if (from.default && to.default) {
|
||||
return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
|
||||
}
|
||||
if (['', this.#helper.getDefaultEmptyString()].includes(to.default) && from.default != null) {
|
||||
return ['', this.#helper.getDefaultEmptyString()].includes(from.default.toString());
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return from.default == to.default; // == intentionally
|
||||
}
|
||||
mapColumnToProperty(column) {
|
||||
const length = /\w+\((\d+)\)/.exec(column.type);
|
||||
const match = /\w+\((\d+), ?(\d+)\)/.exec(column.type);
|
||||
return {
|
||||
fieldNames: [column.name],
|
||||
columnTypes: [column.type],
|
||||
items: column.enumItems,
|
||||
...column,
|
||||
length: length ? +length[1] : column.length,
|
||||
precision: match ? +match[1] : column.precision,
|
||||
scale: match ? +match[2] : column.scale,
|
||||
};
|
||||
}
|
||||
log(message, params) {
|
||||
if (params) {
|
||||
message += ' ' + inspect(params);
|
||||
}
|
||||
this.#logger.log('schema', message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user