diff --git a/integration-tests/__tests__/deploy-database.spec.js b/integration-tests/__tests__/deploy-database.spec.js index 414f1aca..ae140acd 100644 --- a/integration-tests/__tests__/deploy-database.spec.js +++ b/integration-tests/__tests__/deploy-database.spec.js @@ -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'], + }, + }, + ], + ]); + }) + ); }); diff --git a/packages/api/src/shell/generateDeploySql.js b/packages/api/src/shell/generateDeploySql.js index 57b7a645..e99bf616 100644 --- a/packages/api/src/shell/generateDeploySql.js +++ b/packages/api/src/shell/generateDeploySql.js @@ -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; } diff --git a/packages/tools/src/alterPlan.ts b/packages/tools/src/alterPlan.ts index d47100a0..1bbb469f 100644 --- a/packages/tools/src/alterPlan.ts +++ b/packages/tools/src/alterPlan.ts @@ -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 ); diff --git a/packages/tools/src/diffTools.ts b/packages/tools/src/diffTools.ts index 8a85bbf9..01174e21 100644 --- a/packages/tools/src/diffTools.ts +++ b/packages/tools/src/diffTools.ts @@ -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; } }