deploy db diff options

This commit is contained in:
Jan Prochazka 2021-10-14 13:34:27 +02:00
parent aeafa81cb2
commit fa2bb52007
4 changed files with 136 additions and 28 deletions

View File

@ -12,7 +12,7 @@ function checkStructure(structure, model) {
expect(structure.tables.length).toEqual(expected.tables.length);
for (const [realTable, expectedTable] of _.zip(structure.tables, expected.tables)) {
expect(realTable.columns.length).toEqual(expectedTable.columns.length);
expect(realTable.columns.length).toBeGreaterThanOrEqual(expectedTable.columns.length);
}
}
@ -83,4 +83,66 @@ describe('Deploy database', () => {
]);
})
);
test.each(engines.map(engine => [engine.label, engine]))(
'Add column - %s',
testWrapper(async (conn, driver, engine) => {
await testDatabaseDeploy(conn, driver, [
[
{
name: 't1.table.yaml',
json: {
name: 't1',
columns: [{ name: 'id', type: 'int' }],
primaryKey: ['id'],
},
},
],
[
{
name: 't1.table.yaml',
json: {
name: 't1',
columns: [
{ name: 'id', type: 'int' },
{ name: 'val', type: 'int' },
],
primaryKey: ['id'],
},
},
],
]);
})
);
test.each(engines.map(engine => [engine.label, engine]))(
'Dont drop column - %s',
testWrapper(async (conn, driver, engine) => {
await testDatabaseDeploy(conn, driver, [
[
{
name: 't1.table.yaml',
json: {
name: 't1',
columns: [
{ name: 'id', type: 'int' },
{ name: 'val', type: 'int' },
],
primaryKey: ['id'],
},
},
],
[
{
name: 't1.table.yaml',
json: {
name: 't1',
columns: [{ name: 'id', type: 'int' }],
primaryKey: ['id'],
},
},
],
]);
})
);
});

View File

@ -27,8 +27,22 @@ async function generateDeploySql({
extendDatabaseInfo(loadedDbModel ? databaseInfoFromYamlModel(loadedDbModel) : await importDbModel(modelFolder))
);
const currentModel = generateDbPairingId(extendDatabaseInfo(analysedStructure));
const currentModelPaired = matchPairedObjects(deployedModel, currentModel);
const { sql } = getAlterDatabaseScript(currentModelPaired, deployedModel, {}, deployedModel, driver);
const opts = {
ignoreCase: true,
schemaMode: 'ignore',
ignoreConstraintNames: true,
noDropTable: true,
noDropColumn: true,
noDropConstraint: true,
noDropSqlObject: true,
noRenameTable: true,
noRenameColumn: true,
};
const currentModelPaired = matchPairedObjects(deployedModel, currentModel, opts);
// console.log('currentModel', currentModel.tables[0]);
// console.log('currentModelPaired', currentModelPaired.tables[0]);
const { sql } = getAlterDatabaseScript(currentModelPaired, deployedModel, opts, deployedModel, driver);
return sql;
}

View File

@ -1,5 +1,5 @@
import _ from 'lodash';
import { generateTablePairingId } from '.';
import { DbDiffOptions, generateTablePairingId } from './diffTools';
import {
AlterProcessor,
ColumnInfo,
@ -111,7 +111,7 @@ export class AlterPlan {
};
public operations: AlterOperation[] = [];
constructor(public db: DatabaseInfo, public dialect: SqlDialect) {}
constructor(public db: DatabaseInfo, public dialect: SqlDialect, public opts: DbDiffOptions) {}
createTable(table: TableInfo) {
this.operations.push({
@ -365,6 +365,12 @@ export class AlterPlan {
// console.log('*****************RECREATED NEEDED', op, operationType, isAllowed);
// console.log(this.dialect);
if (this.opts.noDropTable) {
// skip this operation, as it cannot be achieved
return [];
}
const table = this.db.tables.find(
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
);

View File

@ -15,15 +15,23 @@ import stableStringify from 'json-stable-stringify';
type DbDiffSchemaMode = 'strict' | 'ignore' | 'ignoreImplicit';
export interface DbDiffOptions {
allowRecreateTable?: boolean;
allowRecreateConstraint?: boolean;
allowRecreateSpecificObject?: boolean;
allowPairRenamedTables?: boolean;
// allowRecreateTable?: boolean;
// allowRecreateConstraint?: boolean;
// allowRecreateSpecificObject?: boolean;
// allowPairRenamedTables?: boolean;
ignoreCase?: boolean;
schemaMode?: DbDiffSchemaMode;
leftImplicitSchema?: string;
rightImplicitSchema?: string;
ignoreConstraintNames?: boolean;
noDropTable?: boolean;
noDropColumn?: boolean;
noDropConstraint?: boolean;
noDropSqlObject?: boolean;
noRenameTable?: boolean;
noRenameColumn?: boolean;
}
export function generateTablePairingId(table: TableInfo): TableInfo {
@ -82,7 +90,7 @@ export function generateDbPairingId(db: DatabaseInfo): DatabaseInfo {
}
function testEqualNames(a: string, b: string, opts: DbDiffOptions) {
if (opts.ignoreCase) return a.toLowerCase() == b.toLowerCase();
if (opts.ignoreCase) return (a || '').toLowerCase() == (b || '').toLowerCase();
return a == b;
}
@ -236,10 +244,14 @@ export function testEqualColumns(
}
function testEqualConstraints(a: ConstraintInfo, b: ConstraintInfo, opts: DbDiffOptions = {}) {
if (a.constraintType=='primaryKey' && b.constraintType=='primaryKey') {
}
return stableStringify(a) == stableStringify(b);
// if (a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey') {
// console.log('PK1', stableStringify(opts.ignoreConstraintNames ? _.omit(a, ['constraintName']) : a));
// console.log('PK2', stableStringify(opts.ignoreConstraintNames ? _.omit(b, ['constraintName']) : b));
// }
return (
stableStringify(opts.ignoreConstraintNames ? _.omit(a, ['constraintName']) : a) ==
stableStringify(opts.ignoreConstraintNames ? _.omit(b, ['constraintName']) : b)
);
}
export function testEqualTypes(a: ColumnInfo, b: ColumnInfo, opts: DbDiffOptions = {}) {
@ -291,10 +303,14 @@ function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInf
(a, b) => a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey'
);
constraintPairs.filter(x => x[1] == null).forEach(x => plan.dropConstraint(x[0]));
columnPairs.filter(x => x[1] == null).forEach(x => plan.dropColumn(x[0]));
if (!opts.noDropConstraint) {
constraintPairs.filter(x => x[1] == null).forEach(x => plan.dropConstraint(x[0]));
}
if (!opts.noDropColumn) {
columnPairs.filter(x => x[1] == null).forEach(x => plan.dropColumn(x[0]));
}
if (!testEqualFullNames(oldTable, newTable, opts)) {
if (!testEqualFullNames(oldTable, newTable, opts) && !opts.noRenameTable) {
plan.renameTable(oldTable, newTable.pureName);
}
@ -304,7 +320,7 @@ function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInf
.filter(x => x[0] && x[1])
.forEach(x => {
if (!testEqualColumns(x[0], x[1], true, true, opts)) {
if (testEqualColumns(x[0], x[1], false, true, opts)) {
if (testEqualColumns(x[0], x[1], false, true, opts) && !opts.noRenameColumn) {
// console.log('PLAN RENAME COLUMN')
plan.renameColumn(x[0], x[1].columnName);
} else {
@ -333,7 +349,7 @@ export function createAlterTablePlan(
db: DatabaseInfo,
driver: EngineDriver
): AlterPlan {
const plan = new AlterPlan(db, driver.dialect);
const plan = new AlterPlan(db, driver.dialect, opts);
if (oldTable == null) {
plan.createTable(newTable);
} else {
@ -350,19 +366,29 @@ export function createAlterDatabasePlan(
db: DatabaseInfo,
driver: EngineDriver
): AlterPlan {
const plan = new AlterPlan(db, driver.dialect);
const plan = new AlterPlan(db, driver.dialect, opts);
for (const objectTypeField of ['tables', 'views', 'procedures', 'matviews', 'functions']) {
for (const oldobj of oldDb[objectTypeField] || []) {
const newobj = (newDb[objectTypeField] || []).find(x => x.pairingId == oldobj.pairingId);
if (objectTypeField == 'tables') {
if (newobj == null) plan.dropTable(oldobj);
else planAlterTable(plan, oldobj, newobj, opts);
if (newobj == null) {
if (!opts.noDropTable) {
plan.dropTable(oldobj);
}
} else {
planAlterTable(plan, oldobj, newobj, opts);
}
} else {
if (newobj == null) plan.dropSqlObject(oldobj);
else if (newobj.createSql != oldobj.createSql) {
if (newobj == null) {
if (!opts.noDropSqlObject) {
plan.dropSqlObject(oldobj);
}
} else if (newobj.createSql != oldobj.createSql) {
plan.recreates.sqlObjects += 1;
plan.dropSqlObject(oldobj);
if (!opts.noDropSqlObject) {
plan.dropSqlObject(oldobj);
}
plan.createSqlObject(newobj);
}
}
@ -416,18 +442,18 @@ export function getAlterDatabaseScript(
};
}
export function matchPairedObjects(db1: DatabaseInfo, db2: DatabaseInfo) {
export function matchPairedObjects(db1: DatabaseInfo, db2: DatabaseInfo, opts: DbDiffOptions) {
const res = _.cloneDeep(db2);
for (const objectTypeField of ['tables', 'views', 'procedures', 'matviews', 'functions']) {
for (const obj2 of res[objectTypeField] || []) {
const obj1 = db1[objectTypeField].find(x => x.pureName == obj2.pureName);
const obj1 = db1[objectTypeField].find(x => testEqualFullNames(x, obj2, opts));
if (obj1) {
obj2.pairingId = obj1.pairingId;
if (objectTypeField == 'tables') {
for (const col2 of obj2.columns) {
const col1 = obj1.columns.find(x => x.columnName == col2.columnName);
const col1 = obj1.columns.find(x => testEqualNames(x.columnName, col2.columnName, opts));
if (col1) col2.pairingId = col1.pairingId;
}
}