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); expect(structure.tables.length).toEqual(expected.tables.length);
for (const [realTable, expectedTable] of _.zip(structure.tables, expected.tables)) { 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)) extendDatabaseInfo(loadedDbModel ? databaseInfoFromYamlModel(loadedDbModel) : await importDbModel(modelFolder))
); );
const currentModel = generateDbPairingId(extendDatabaseInfo(analysedStructure)); const currentModel = generateDbPairingId(extendDatabaseInfo(analysedStructure));
const currentModelPaired = matchPairedObjects(deployedModel, currentModel); const opts = {
const { sql } = getAlterDatabaseScript(currentModelPaired, deployedModel, {}, deployedModel, driver); 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; return sql;
} }

View File

@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { generateTablePairingId } from '.'; import { DbDiffOptions, generateTablePairingId } from './diffTools';
import { import {
AlterProcessor, AlterProcessor,
ColumnInfo, ColumnInfo,
@ -111,7 +111,7 @@ export class AlterPlan {
}; };
public operations: AlterOperation[] = []; public operations: AlterOperation[] = [];
constructor(public db: DatabaseInfo, public dialect: SqlDialect) {} constructor(public db: DatabaseInfo, public dialect: SqlDialect, public opts: DbDiffOptions) {}
createTable(table: TableInfo) { createTable(table: TableInfo) {
this.operations.push({ this.operations.push({
@ -365,6 +365,12 @@ export class AlterPlan {
// console.log('*****************RECREATED NEEDED', op, operationType, isAllowed); // console.log('*****************RECREATED NEEDED', op, operationType, isAllowed);
// console.log(this.dialect); // console.log(this.dialect);
if (this.opts.noDropTable) {
// skip this operation, as it cannot be achieved
return [];
}
const table = this.db.tables.find( const table = this.db.tables.find(
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName 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'; type DbDiffSchemaMode = 'strict' | 'ignore' | 'ignoreImplicit';
export interface DbDiffOptions { export interface DbDiffOptions {
allowRecreateTable?: boolean; // allowRecreateTable?: boolean;
allowRecreateConstraint?: boolean; // allowRecreateConstraint?: boolean;
allowRecreateSpecificObject?: boolean; // allowRecreateSpecificObject?: boolean;
allowPairRenamedTables?: boolean; // allowPairRenamedTables?: boolean;
ignoreCase?: boolean; ignoreCase?: boolean;
schemaMode?: DbDiffSchemaMode; schemaMode?: DbDiffSchemaMode;
leftImplicitSchema?: string; leftImplicitSchema?: string;
rightImplicitSchema?: string; rightImplicitSchema?: string;
ignoreConstraintNames?: boolean;
noDropTable?: boolean;
noDropColumn?: boolean;
noDropConstraint?: boolean;
noDropSqlObject?: boolean;
noRenameTable?: boolean;
noRenameColumn?: boolean;
} }
export function generateTablePairingId(table: TableInfo): TableInfo { export function generateTablePairingId(table: TableInfo): TableInfo {
@ -82,7 +90,7 @@ export function generateDbPairingId(db: DatabaseInfo): DatabaseInfo {
} }
function testEqualNames(a: string, b: string, opts: DbDiffOptions) { 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; return a == b;
} }
@ -236,10 +244,14 @@ export function testEqualColumns(
} }
function testEqualConstraints(a: ConstraintInfo, b: ConstraintInfo, opts: DbDiffOptions = {}) { function testEqualConstraints(a: ConstraintInfo, b: ConstraintInfo, opts: DbDiffOptions = {}) {
if (a.constraintType=='primaryKey' && b.constraintType=='primaryKey') { // 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(a) == stableStringify(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 = {}) { 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' (a, b) => a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey'
); );
constraintPairs.filter(x => x[1] == null).forEach(x => plan.dropConstraint(x[0])); if (!opts.noDropConstraint) {
columnPairs.filter(x => x[1] == null).forEach(x => plan.dropColumn(x[0])); 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); plan.renameTable(oldTable, newTable.pureName);
} }
@ -304,7 +320,7 @@ function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInf
.filter(x => x[0] && x[1]) .filter(x => x[0] && x[1])
.forEach(x => { .forEach(x => {
if (!testEqualColumns(x[0], x[1], true, true, opts)) { 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') // console.log('PLAN RENAME COLUMN')
plan.renameColumn(x[0], x[1].columnName); plan.renameColumn(x[0], x[1].columnName);
} else { } else {
@ -333,7 +349,7 @@ export function createAlterTablePlan(
db: DatabaseInfo, db: DatabaseInfo,
driver: EngineDriver driver: EngineDriver
): AlterPlan { ): AlterPlan {
const plan = new AlterPlan(db, driver.dialect); const plan = new AlterPlan(db, driver.dialect, opts);
if (oldTable == null) { if (oldTable == null) {
plan.createTable(newTable); plan.createTable(newTable);
} else { } else {
@ -350,19 +366,29 @@ export function createAlterDatabasePlan(
db: DatabaseInfo, db: DatabaseInfo,
driver: EngineDriver driver: EngineDriver
): AlterPlan { ): 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 objectTypeField of ['tables', 'views', 'procedures', 'matviews', 'functions']) {
for (const oldobj of oldDb[objectTypeField] || []) { for (const oldobj of oldDb[objectTypeField] || []) {
const newobj = (newDb[objectTypeField] || []).find(x => x.pairingId == oldobj.pairingId); const newobj = (newDb[objectTypeField] || []).find(x => x.pairingId == oldobj.pairingId);
if (objectTypeField == 'tables') { if (objectTypeField == 'tables') {
if (newobj == null) plan.dropTable(oldobj); if (newobj == null) {
else planAlterTable(plan, oldobj, newobj, opts); if (!opts.noDropTable) {
plan.dropTable(oldobj);
}
} else {
planAlterTable(plan, oldobj, newobj, opts);
}
} else { } else {
if (newobj == null) plan.dropSqlObject(oldobj); if (newobj == null) {
else if (newobj.createSql != oldobj.createSql) { if (!opts.noDropSqlObject) {
plan.dropSqlObject(oldobj);
}
} else if (newobj.createSql != oldobj.createSql) {
plan.recreates.sqlObjects += 1; plan.recreates.sqlObjects += 1;
plan.dropSqlObject(oldobj); if (!opts.noDropSqlObject) {
plan.dropSqlObject(oldobj);
}
plan.createSqlObject(newobj); 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); const res = _.cloneDeep(db2);
for (const objectTypeField of ['tables', 'views', 'procedures', 'matviews', 'functions']) { for (const objectTypeField of ['tables', 'views', 'procedures', 'matviews', 'functions']) {
for (const obj2 of res[objectTypeField] || []) { 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) { if (obj1) {
obj2.pairingId = obj1.pairingId; obj2.pairingId = obj1.pairingId;
if (objectTypeField == 'tables') { if (objectTypeField == 'tables') {
for (const col2 of obj2.columns) { 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; if (col1) col2.pairingId = col1.pairingId;
} }
} }