Alexandre Bodin c173fa416e wip
2021-09-15 19:53:15 +02:00

356 lines
10 KiB
JavaScript

'use strict';
const _ = require('lodash/fp');
const statuses = {
CHANGED: 'CHANGED',
UNCHANGED: 'UNCHANGED',
};
// NOTE:We could move the schema to use maps of tables & columns instead of arrays to make it easier to diff
// => this will make the creation a bit more complicated (ordering, Object.values(tables | columns)) -> not a big pbl
const helpers = {
hasTable(schema, tableName) {
return schema.tables.findIndex(table => table.name === tableName) !== -1;
},
findTable(schema, tableName) {
return schema.tables.find(table => table.name === tableName);
},
hasColumn(table, columnName) {
return table.columns.findIndex(column => column.name === columnName) !== -1;
},
findColumn(table, columnName) {
return table.columns.find(column => column.name === columnName);
},
hasIndex(table, columnName) {
return table.indexes.findIndex(column => column.name === columnName) !== -1;
},
findIndex(table, columnName) {
return table.indexes.find(column => column.name === columnName);
},
hasForeignKey(table, columnName) {
return table.foreignKeys.findIndex(column => column.name === columnName) !== -1;
},
findForeignKey(table, columnName) {
return table.foreignKeys.find(column => column.name === columnName);
},
};
module.exports = db => {
const hasChangedStatus = diff => diff.status === statuses.CHANGED;
const diffProperties = (srcObject, destObject) => {
const addedProperties = [];
const updatedProperties = [];
const unchangedProperties = [];
const removedProperties = [];
for (const key in destObject) {
const value = destObject[key];
if (_.has(key, srcObject)) {
const srcValue = srcObject[key];
if (_.isEqual(srcValue, value)) {
unchangedProperties.push({ key, value });
} else {
updatedProperties.push({ key, oldValue: srcValue, value });
}
} else {
addedProperties.push({ key, value });
}
}
for (const key in srcObject) {
const value = srcObject[key];
if (!_.has(key, destObject)) {
removedProperties.push({ key, oldValue: value });
}
}
const hasChanged = [addedProperties, updatedProperties, removedProperties].some(
arr => arr.length > 0
);
return {
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
name: destObject.name,
object: destObject,
// NOTE: maybe put into properties: {}
added: addedProperties,
updated: updatedProperties,
unchanged: unchangedProperties,
removed: removedProperties,
},
};
};
const diffDefault = (oldColumn, column) => {
const oldDefaultTo = oldColumn.defaultTo;
const defaultTo = column.defaultTo;
if (oldDefaultTo === null || _.toLower(oldDefaultTo) === 'null') {
return _.isNil(defaultTo) || _.toLower(defaultTo) === 'null';
}
if (!_.isNil(oldDefaultTo) && !_.isNil(defaultTo)) {
return _.toLower(oldDefaultTo) === _.toLower(`'${column.defaultTo}'`);
}
return oldDefaultTo === defaultTo;
};
/**
* Compares two columns info
* @param {Object} oldColumn - column info read from DB
* @param {Object} column - newly generate column info
*/
const diffColumns = (oldColumn, column) => {
const changes = [];
// NOTE: we might want to move that to the schema generation instead
// NOTE: enum aren't updated, they need to be dropped & recreated. Knex doesn't handle it
const oldType = oldColumn.type;
const type = db.dialect.getSqlType(column.type);
if (oldType !== type && !['increments', 'enum'].includes(type)) {
changes.push('type');
}
if (!_.isEqual(oldColumn.args, column.args) && !['increments', 'enum'].includes(type)) {
changes.push('args');
}
if (oldColumn.notNullable !== column.notNullable) {
changes.push('notNullable');
}
const hasSameDefault = diffDefault(oldColumn, column);
if (!hasSameDefault) {
changes.push('defaultTo');
}
if (oldColumn.unsigned !== column.unsigned && db.dialect.supportsUnsigned()) {
changes.push('unsigned');
}
if (changes.length > 0) {
console.log(`Changes in ${column.name}, ${changes}`);
}
return {
status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
name: column.name,
object: column,
},
};
};
const diffTableColumns = (srcTable, destTable) => {
const addedColumns = [];
const updatedColumns = [];
const unchangedColumns = [];
const removedColumns = [];
for (const destColumn of destTable.columns) {
if (!helpers.hasColumn(srcTable, destColumn.name)) {
addedColumns.push(destColumn);
continue;
}
const srcColumn = helpers.findColumn(srcTable, destColumn.name);
const { status, diff } = diffColumns(srcColumn, destColumn);
if (status === statuses.CHANGED) {
updatedColumns.push(diff);
} else {
unchangedColumns.push(srcColumn);
}
}
for (const srcColumn of srcTable.columns) {
if (!helpers.hasColumn(destTable, srcColumn.name)) {
removedColumns.push(srcColumn);
}
}
const hasChanged = [addedColumns, updatedColumns, removedColumns].some(arr => arr.length > 0);
return {
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
added: addedColumns,
updated: updatedColumns,
unchanged: unchangedColumns,
removed: removedColumns,
},
};
};
const diffTableIndexes = (srcTable, destTable) => {
const addedIndexes = [];
const updatedIndexes = [];
const unchangedIndexes = [];
const removedIndexes = [];
for (const destIndex of destTable.indexes) {
if (helpers.hasIndex(srcTable, destIndex.name)) {
const srcIndex = helpers.findIndex(srcTable, destIndex.name);
const { status, diff } = diffProperties(srcIndex, destIndex);
if (status === statuses.CHANGED) {
updatedIndexes.push(diff);
} else {
unchangedIndexes.push(srcIndex);
}
} else {
addedIndexes.push(destIndex);
}
}
for (const srcIndex of srcTable.indexes) {
if (!helpers.hasIndex(destTable, srcIndex.name)) {
removedIndexes.push(srcIndex);
}
}
const hasChanged = [addedIndexes, updatedIndexes, removedIndexes].some(arr => arr.length > 0);
return {
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
added: addedIndexes,
updated: updatedIndexes,
unchanged: unchangedIndexes,
removed: removedIndexes,
},
};
};
const diffTableForeignKeys = (srcTable, destTable) => {
const addedForeignKeys = [];
const updatedForeignKeys = [];
const unchangedForeignKeys = [];
const removedForeignKeys = [];
if (!db.dialect.usesForeignKeys()) {
return {
status: statuses.UNCHANGED,
diff: {
added: addedForeignKeys,
updated: updatedForeignKeys,
unchanged: unchangedForeignKeys,
removed: removedForeignKeys,
},
};
}
for (const destForeignKey of destTable.foreignKeys) {
if (helpers.hasForeignKey(srcTable, destForeignKey.name)) {
const srcForeignKey = helpers.findForeignKey(srcTable, destForeignKey.name);
const { status, diff } = diffProperties(srcForeignKey, destForeignKey);
if (status === statuses.CHANGED) {
updatedForeignKeys.push(diff);
} else {
unchangedForeignKeys.push(srcForeignKey);
}
} else {
addedForeignKeys.push(destForeignKey);
}
}
for (const srcForeignKey of srcTable.foreignKeys) {
if (!helpers.hasForeignKey(destTable, srcForeignKey.name)) {
removedForeignKeys.push(srcForeignKey);
}
}
const hasChanged = [addedForeignKeys, updatedForeignKeys, removedForeignKeys].some(
arr => arr.length > 0
);
return {
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
added: addedForeignKeys,
updated: updatedForeignKeys,
unchanged: unchangedForeignKeys,
removed: removedForeignKeys,
},
};
};
const diffTables = (srcTable, destTable) => {
const columnsDiff = diffTableColumns(srcTable, destTable);
const indexesDiff = diffTableIndexes(srcTable, destTable);
const foreignKeysDiff = diffTableForeignKeys(srcTable, destTable);
const hasChanged = [columnsDiff, indexesDiff, foreignKeysDiff].some(hasChangedStatus);
return {
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
name: srcTable.name,
indexes: indexesDiff.diff,
foreignKeys: foreignKeysDiff.diff,
columns: columnsDiff.diff,
},
};
};
const diffSchemas = (srcSchema, destSchema) => {
const addedTables = [];
const updatedTables = [];
const unchangedTables = [];
const removedTables = [];
for (const destTable of destSchema.tables) {
if (helpers.hasTable(srcSchema, destTable.name)) {
// either changed or unchanged
const srcTable = helpers.findTable(srcSchema, destTable.name);
const { status, diff } = diffTables(srcTable, destTable);
if (status === statuses.CHANGED) {
updatedTables.push(diff);
} else {
unchangedTables.push(srcTable);
}
} else {
addedTables.push(destTable);
}
}
for (const srcTable of srcSchema.tables) {
if (!helpers.hasTable(destSchema, srcTable.name)) {
removedTables.push(srcTable);
}
}
const hasChanged = [addedTables, updatedTables, removedTables].some(arr => arr.length > 0);
return {
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
diff: {
tables: {
added: addedTables,
updated: updatedTables,
unchanged: unchangedTables,
removed: removedTables,
},
},
};
};
return {
diff: diffSchemas,
};
};