diff --git a/integration-tests/__tests__/deploy-database.spec.js b/integration-tests/__tests__/deploy-database.spec.js index ae140acd7..781cb8228 100644 --- a/integration-tests/__tests__/deploy-database.spec.js +++ b/integration-tests/__tests__/deploy-database.spec.js @@ -11,14 +11,18 @@ function checkStructure(structure, model) { const expected = databaseInfoFromYamlModel(model); expect(structure.tables.length).toEqual(expected.tables.length); - for (const [realTable, expectedTable] of _.zip(structure.tables, expected.tables)) { + for (const [realTable, expectedTable] of _.zip( + _.sortBy(structure.tables, 'pureName'), + _.sortBy(expected.tables, 'pureName') + )) { expect(realTable.columns.length).toBeGreaterThanOrEqual(expectedTable.columns.length); } } -async function testDatabaseDeploy(conn, driver, dbModelsYaml) { +async function testDatabaseDeploy(conn, driver, dbModelsYaml, testEmptyLastScript) { + let index = 0; for (const loadedDbModel of dbModelsYaml) { - const sql = await generateDeploySql({ + const { sql, isEmpty } = await generateDeploySql({ systemConnection: conn, driver, loadedDbModel, @@ -26,11 +30,18 @@ async function testDatabaseDeploy(conn, driver, dbModelsYaml) { console.debug('Generated deploy script:', sql); expect(sql.toUpperCase().includes('DROP ')).toBeFalsy(); + console.log('dbModelsYaml.length', dbModelsYaml.length, index); + if (testEmptyLastScript && index == dbModelsYaml.length - 1) { + expect(isEmpty).toBeTruthy(); + } + await deployDb({ systemConnection: conn, driver, loadedDbModel, }); + + index++; } const structure = await driver.analyseFull(conn); @@ -59,28 +70,33 @@ describe('Deploy database', () => { test.each(engines.map(engine => [engine.label, engine]))( 'Deploy database simple twice - %s', testWrapper(async (conn, driver, engine) => { - await testDatabaseDeploy(conn, driver, [ + 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' }], + primaryKey: ['id'], + }, }, - }, - ], - [ - { - 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' }], + primaryKey: ['id'], + }, }, - }, + ], ], - ]); + true + ); }) ); @@ -118,31 +134,91 @@ describe('Deploy database', () => { test.each(engines.map(engine => [engine.label, engine]))( 'Dont drop column - %s', testWrapper(async (conn, driver, engine) => { - await testDatabaseDeploy(conn, driver, [ + 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' }, + { name: 'val', type: 'int' }, + ], + primaryKey: ['id'], + }, }, - }, + ], + [ + { + name: 't1.table.yaml', + json: { + name: 't1', + columns: [{ name: 'id', type: 'int' }], + primaryKey: ['id'], + }, + }, + ], ], + true + ); + }) + ); + + test.each(engines.map(engine => [engine.label, engine]))( + 'Foreign keys - %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: 't2.table.yaml', + json: { + name: 't2', + columns: [ + { name: 't2id', type: 'int' }, + { name: 't1id', type: 'int', references: 't1' }, + ], + primaryKey: ['t2id'], + }, }, - }, + { + name: 't1.table.yaml', + json: { + name: 't1', + columns: [{ name: 't1id', type: 'int' }], + primaryKey: ['t1id'], + }, + }, + ], + [ + { + name: 't2.table.yaml', + json: { + name: 't2', + columns: [ + { name: 't2id', type: 'int' }, + { name: 't1id', type: 'int', references: 't1' }, + ], + primaryKey: ['t2id'], + }, + }, + { + name: 't1.table.yaml', + json: { + name: 't1', + columns: [{ name: 't1id', type: 'int' }], + primaryKey: ['t1id'], + }, + }, + ], ], - ]); + true + ); }) ); }); diff --git a/packages/api/src/shell/deployDb.js b/packages/api/src/shell/deployDb.js index 8c2348fcf..03b96f263 100644 --- a/packages/api/src/shell/deployDb.js +++ b/packages/api/src/shell/deployDb.js @@ -2,7 +2,7 @@ const generateDeploySql = require('./generateDeploySql'); const executeQuery = require('./executeQuery'); async function deployDb({ connection, systemConnection, driver, analysedStructure, modelFolder, loadedDbModel }) { - const sql = await generateDeploySql({ + const { sql } = await generateDeploySql({ connection, systemConnection, driver, diff --git a/packages/api/src/shell/generateDeploySql.js b/packages/api/src/shell/generateDeploySql.js index e99bf616b..63d72ea0b 100644 --- a/packages/api/src/shell/generateDeploySql.js +++ b/packages/api/src/shell/generateDeploySql.js @@ -38,12 +38,14 @@ async function generateDeploySql({ noDropSqlObject: true, noRenameTable: true, noRenameColumn: true, + ignoreForeignKeyActions: true, }; const currentModelPaired = matchPairedObjects(deployedModel, currentModel, opts); + // console.log('deployedModel', deployedModel.tables[0]); // console.log('currentModel', currentModel.tables[0]); // console.log('currentModelPaired', currentModelPaired.tables[0]); - const { sql } = getAlterDatabaseScript(currentModelPaired, deployedModel, opts, deployedModel, driver); - return sql; + const res = getAlterDatabaseScript(currentModelPaired, deployedModel, opts, deployedModel, driver); + return res; } module.exports = generateDeploySql; diff --git a/packages/tools/src/alterPlan.ts b/packages/tools/src/alterPlan.ts index 1bbb469f5..2b4d965f0 100644 --- a/packages/tools/src/alterPlan.ts +++ b/packages/tools/src/alterPlan.ts @@ -244,6 +244,10 @@ export class AlterPlan { if (op.operationType == 'dropColumn') { const constraints = this._getDependendColumnConstraints(op.oldObject, this.dialect.dropColumnDependencies); + if (constraints.length > 0 && this.opts.noDropConstraint) { + return []; + } + const res: AlterOperation[] = [ ...constraints.map(oldObject => { const opRes: AlterOperation = { @@ -260,6 +264,10 @@ export class AlterPlan { if (op.operationType == 'changeColumn') { const constraints = this._getDependendColumnConstraints(op.oldObject, this.dialect.changeColumnDependencies); + if (constraints.length > 0 && this.opts.noDropConstraint) { + return []; + } + const res: AlterOperation[] = [ ...constraints.map(oldObject => { const opRes: AlterOperation = { @@ -300,6 +308,11 @@ export class AlterPlan { } if (op.operationType == 'changeConstraint') { + if (this.opts.noDropConstraint) { + // skip constraint recreate + return []; + } + this.recreates.constraints += 1; const opDrop: AlterOperation = { operationType: 'dropConstraint', @@ -418,6 +431,37 @@ export class AlterPlan { return res; } + _moveForeignKeysToLast(): AlterOperation[] { + if (!this.dialect.createForeignKey) { + return this.operations; + } + const fks = []; + const res = this.operations.map(op => { + if (op.operationType == 'createTable') { + fks.push(...(op.newObject.foreignKeys || [])); + return { + ...op, + newObject: { + ...op.newObject, + foreignKeys: [], + }, + }; + } + return op; + }); + + return [ + ...res, + ...fks.map( + fk => + ({ + operationType: 'createConstraint', + newObject: fk, + } as AlterOperation_CreateConstraint) + ), + ]; + } + transformPlan() { // console.log('*****************OPERATIONS0', this.operations); @@ -432,6 +476,10 @@ export class AlterPlan { this.operations = this._groupTableRecreations(); // console.log('*****************OPERATIONS3', this.operations); + + this.operations = this._moveForeignKeysToLast(); + + // console.log('*****************OPERATIONS4', this.operations); } } diff --git a/packages/tools/src/diffTools.ts b/packages/tools/src/diffTools.ts index bdaae1a58..707920f5d 100644 --- a/packages/tools/src/diffTools.ts +++ b/packages/tools/src/diffTools.ts @@ -7,10 +7,13 @@ import { SqlDialect, TableInfo, } from 'dbgate-types'; -import _ from 'lodash'; import uuidv1 from 'uuid/v1'; import { AlterPlan } from './alterPlan'; import stableStringify from 'json-stable-stringify'; +import _omit from 'lodash/omit'; +import _cloneDeep from 'lodash/cloneDeep'; +import _isEqual from 'lodash/isEqual'; +import _pick from 'lodash/pick'; type DbDiffSchemaMode = 'strict' | 'ignore' | 'ignoreImplicit'; @@ -32,6 +35,7 @@ export interface DbDiffOptions { noDropSqlObject?: boolean; noRenameTable?: boolean; noRenameColumn?: boolean; + ignoreForeignKeyActions?: boolean; } export function generateTablePairingId(table: TableInfo): TableInfo { @@ -245,15 +249,29 @@ export function testEqualColumns( function testEqualConstraints(a: ConstraintInfo, b: ConstraintInfo, opts: DbDiffOptions = {}) { const omitList = []; - if (opts.ignoreConstraintNames) omitList.push('constraintName'); - if (opts.schemaMode == 'ignore') omitList.push('schemaName'); + if (opts.ignoreForeignKeyActions) { + omitList.push('updateAction'); + omitList.push('deleteAction'); + } + if (opts.ignoreConstraintNames) { + omitList.push('constraintName'); + } + if (opts.schemaMode == 'ignore') { + omitList.push('schemaName'); + omitList.push('refSchemaName'); + } // if (a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey') { // console.log('PK1', stableStringify(_.omit(a, omitList))); // console.log('PK2', stableStringify(_.omit(b, omitList))); // } - - return stableStringify(_.omit(a, omitList)) == stableStringify(_.omit(b, omitList)); + + // if (a.constraintType == 'foreignKey' && b.constraintType == 'foreignKey') { + // console.log('FK1', stableStringify(_omit(a, omitList))); + // console.log('FK2', stableStringify(_omit(b, omitList))); + // } + + return stableStringify(_omit(a, omitList)) == stableStringify(_omit(b, omitList)); } export function testEqualTypes(a: ColumnInfo, b: ColumnInfo, opts: DbDiffOptions = {}) { @@ -441,11 +459,12 @@ export function getAlterDatabaseScript( return { sql: dmp.s, recreates: plan.recreates, + isEmpty: plan.operations.length == 0, }; } 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 obj2 of res[objectTypeField] || []) { @@ -458,6 +477,18 @@ export function matchPairedObjects(db1: DatabaseInfo, db2: DatabaseInfo, opts: D const col1 = obj1.columns.find(x => testEqualNames(x.columnName, col2.columnName, opts)); if (col1) col2.pairingId = col1.pairingId; } + + for (const fk2 of obj2.foreignKeys) { + const fk1 = obj1.foreignKeys.find( + x => + testEqualNames(x.refTableName, fk2.refTableName, opts) && + _isEqual( + x.columns.map(y => _pick(y, ['columnName', 'refColumnName'])), + fk2.columns.map(y => _pick(y, ['columnName', 'refColumnName'])) + ) + ); + if (fk1) fk2.pairingId = fk1.pairingId; + } } } } diff --git a/packages/web/src/appobj/ArchiveFolderAppObject.svelte b/packages/web/src/appobj/ArchiveFolderAppObject.svelte index 5185f7cf1..35827e334 100644 --- a/packages/web/src/appobj/ArchiveFolderAppObject.svelte +++ b/packages/web/src/appobj/ArchiveFolderAppObject.svelte @@ -43,7 +43,7 @@ archiveFolder: data.name, }); - newQuery({ initialData: resp.data }); + newQuery({ initialData: resp.data.sql }); }; function createMenu() {