Initial commit - Event Planner application

This commit is contained in:
mberlin
2026-03-18 14:55:56 -03:00
commit 86d779eb4d
7548 changed files with 1006324 additions and 0 deletions

753
node_modules/@mikro-orm/sql/schema/SchemaComparator.js generated vendored Normal file
View 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);
}
}