diff --git a/integration-tests/__tests__/alter-processor.spec.js b/integration-tests/__tests__/alter-processor.spec.js new file mode 100644 index 000000000..d6caffa2f --- /dev/null +++ b/integration-tests/__tests__/alter-processor.spec.js @@ -0,0 +1,33 @@ +const engines = require('../engines'); +const uuidv1 = require('uuid/v1'); +const { testWrapper } = require('../tools'); + +async function testTableDiff(conn, driver, mangle) { + await driver.query(conn, 'create table t1 (col1 int not null)'); + + const structure1 = await driver.analyseFull(conn); + mangle(structure1.tables[0]); +} + +describe('Alter processor', () => { + test.each(engines.map(engine => [engine.label, engine]))( + 'Add column - %s', + testWrapper(async (conn, driver, engine) => { + testTableDiff(conn, driver, tbl => + tbl.columns.push({ + columnName: 'added', + dataType: 'int', + pairingId: uuidv1(), + }) + ); + // console.log('ENGINE', engine); + // for (const sql of initSql) await driver.query(conn, sql); + + // await driver.query(conn, object.create1); + // const structure = await driver.analyseFull(conn); + + // expect(structure[type].length).toEqual(1); + // expect(structure[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match); + }) + ); +}); diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index 385bf9e15..7daa6dd32 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -15,12 +15,13 @@ import { IndexInfo, UniqueInfo, CheckInfo, + AlterProcessor, } from 'dbgate-types'; import _isString from 'lodash/isString'; import _isNumber from 'lodash/isNumber'; import _isDate from 'lodash/isDate'; -export class SqlDumper { +export class SqlDumper implements AlterProcessor { s = ''; driver: EngineDriver; dialect: SqlDialect; @@ -416,8 +417,8 @@ export class SqlDumper { renameConstraint(constraint: ConstraintInfo, newName: string) {} - createColumn(table: TableInfo, column: ColumnInfo, constraints: ConstraintInfo[]) { - this.put('^alter ^table %f ^add %i ', table, column.columnName); + createColumn(column: ColumnInfo, constraints: ConstraintInfo[]) { + this.put('^alter ^table %f ^add %i ', column, column.columnName); this.columnDefinition(column); this.inlineConstraints(constraints); this.endCommand(); diff --git a/packages/tools/src/alterPlan.ts b/packages/tools/src/alterPlan.ts index 350d635b0..ca57cf8e9 100644 --- a/packages/tools/src/alterPlan.ts +++ b/packages/tools/src/alterPlan.ts @@ -1,4 +1,4 @@ -import { ColumnInfo, ConstraintInfo, DatabaseInfo, TableInfo } from '../../types'; +import { AlterProcessor, ColumnInfo, ConstraintInfo, DatabaseInfo, NamedObjectInfo, TableInfo } from '../../types'; interface AlterOperation_CreateTable { operationType: 'createTable'; @@ -10,6 +10,12 @@ interface AlterOperation_DropTable { oldObject: TableInfo; } +interface AlterOperation_RenameTable { + operationType: 'renameTable'; + object: TableInfo; + newName: string; +} + interface AlterOperation_CreateColumn { operationType: 'createColumn'; newObject: ColumnInfo; @@ -21,6 +27,12 @@ interface AlterOperation_ChangeColumn { newObject: ColumnInfo; } +interface AlterOperation_RenameColumn { + operationType: 'renameColumn'; + object: ColumnInfo; + newName: string; +} + interface AlterOperation_DropColumn { operationType: 'dropColumn'; oldObject: ColumnInfo; @@ -42,6 +54,12 @@ interface AlterOperation_DropConstraint { oldObject: ConstraintInfo; } +interface AlterOperation_RenameConstraint { + operationType: 'renameConstraint'; + object: ConstraintInfo; + newName: string; +} + type AlterOperation = | AlterOperation_CreateColumn | AlterOperation_ChangeColumn @@ -50,7 +68,10 @@ type AlterOperation = | AlterOperation_ChangeConstraint | AlterOperation_DropConstraint | AlterOperation_CreateTable - | AlterOperation_DropTable; + | AlterOperation_DropTable + | AlterOperation_RenameTable + | AlterOperation_RenameColumn + | AlterOperation_RenameConstraint; export class AlterPlan { operations: AlterOperation[] = []; @@ -114,4 +135,53 @@ export class AlterPlan { }); } + renameTable(table: TableInfo, newName: string) { + this.operations.push({ + operationType: 'renameTable', + object: table, + newName, + }); + } + + renameColumn(column: ColumnInfo, newName: string) { + this.operations.push({ + operationType: 'renameColumn', + object: column, + newName, + }); + } + + renameConstraint(constraint: ConstraintInfo, newName: string) { + this.operations.push({ + operationType: 'renameConstraint', + object: constraint, + newName, + }); + } +} + +export function runAlterOperation(op: AlterOperation, processor: AlterProcessor) { + switch (op.operationType) { + case 'createTable': + processor.createTable(op.newObject); + break; + case 'changeColumn': + processor.changeColumn(op.oldObject, op.newObject); + break; + case 'createColumn': + processor.createColumn(op.newObject, []); + break; + case 'dropColumn': + processor.dropColumn(op.oldObject); + break; + case 'changeConstraint': + processor.changeConstraint(op.oldObject, op.newObject); + break; + case 'createConstraint': + processor.createConstraint(op.newObject); + break; + case 'dropConstraint': + processor.dropConstraint(op.oldObject); + break; + } } diff --git a/packages/tools/src/database-info-alter-processor.ts b/packages/tools/src/database-info-alter-processor.ts new file mode 100644 index 000000000..e8c304dc4 --- /dev/null +++ b/packages/tools/src/database-info-alter-processor.ts @@ -0,0 +1,69 @@ +import { ColumnInfo, ConstraintInfo, DatabaseInfo, ForeignKeyInfo, PrimaryKeyInfo, TableInfo } from '../../types'; + +export class DatabaseInfoAlterProcessor { + constructor(public db: DatabaseInfo) {} + + createTable(table: TableInfo) { + this.db.tables.push(table); + } + + dropTable(table: TableInfo) { + this.db.tables = this.db.tables.filter(x => x.pureName != table.pureName && x.schemaName != table.schemaName); + } + + createColumn(column: ColumnInfo) { + const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName); + table.columns.push(column); + } + + changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo) { + const table = this.db.tables.find(x => x.pureName == oldColumn.pureName && x.schemaName == oldColumn.schemaName); + table.columns = table.columns.map(x => (x.columnName == oldColumn.columnName ? newColumn : x)); + } + + dropColumn(column: ColumnInfo) { + const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName); + table.columns = table.columns.filter(x => x.columnName != column.columnName); + } + + createConstraint(constraint: ConstraintInfo) { + const table = this.db.tables.find(x => x.pureName == constraint.pureName && x.schemaName == constraint.schemaName); + switch (constraint.constraintType) { + case 'primaryKey': + table.primaryKey = constraint as PrimaryKeyInfo; + break; + case 'foreignKey': + table.foreignKeys.push(constraint as ForeignKeyInfo); + break; + } + } + + changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo) { + const table = this.db.tables.find( + x => x.pureName == oldConstraint.pureName && x.schemaName == oldConstraint.schemaName + ); + } + + dropConstraint(constraint: ConstraintInfo) { + const table = this.db.tables.find(x => x.pureName == constraint.pureName && x.schemaName == constraint.schemaName); + switch (constraint.constraintType) { + case 'primaryKey': + table.primaryKey = null; + break; + case 'foreignKey': + table.foreignKeys = table.foreignKeys.filter(x => x.constraintName != constraint.constraintName); + break; + } + } + + renameTable(table: TableInfo, newName: string) { + this.db.tables.find(x => x.pureName == table.pureName && x.schemaName == table.schemaName).pureName = newName; + } + + renameColumn(column: ColumnInfo, newName: string) { + const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName); + table.columns.find(x => x.columnName == column.columnName).columnName = newName; + } + + renameConstraint(constraint: ConstraintInfo, newName: string) {} +} diff --git a/packages/tools/src/diffTools.ts b/packages/tools/src/diffTools.ts index cd0849a18..87b7598d7 100644 --- a/packages/tools/src/diffTools.ts +++ b/packages/tools/src/diffTools.ts @@ -1,12 +1,19 @@ -import { ColumnInfo, DatabaseInfo, TableInfo } from 'dbgate-types'; +import { ColumnInfo, DatabaseInfo, EngineDriver, NamedObjectInfo, TableInfo } from 'dbgate-types'; import uuidv1 from 'uuid/v1'; import { AlterPlan } from './alterPlan'; +type DbDiffSchemaMode = 'strict' | 'ignore' | 'ignoreImplicit'; + export interface DbDiffOptions { allowRecreateTable: boolean; allowRecreateConstraint: boolean; allowRecreateSpecificObject: boolean; allowPairRenamedTables: boolean; + + ignoreCase: boolean; + schemaMode: DbDiffSchemaMode; + leftImplicitSchema: string; + rightImplicitSchema: string; } export function generateTablePairingId(table: TableInfo): TableInfo { @@ -36,6 +43,160 @@ export function generateTablePairingId(table: TableInfo): TableInfo { return table; } +function testEqualNames(a: string, b: string, opts: DbDiffOptions) { + if (opts.ignoreCase) return a.toLowerCase() == b.toLowerCase(); + return a == b; +} + +function testEqualSchemas(lschema: string, rschema: string, opts: DbDiffOptions) { + if (opts.schemaMode == 'ignore') lschema = null; + if (opts.schemaMode == 'ignoreImplicit' && lschema == opts.leftImplicitSchema) lschema = null; + if (opts.schemaMode == 'ignore') rschema = null; + if (opts.schemaMode == 'ignoreImplicit' && rschema == opts.rightImplicitSchema) rschema = null; + return testEqualNames(lschema, rschema, opts); +} + +function testEqualFullNames(lft: NamedObjectInfo, rgt: NamedObjectInfo, opts: DbDiffOptions) { + if (lft == null || rgt == null) return lft == rgt; + return testEqualSchemas(lft.schemaName, rgt.schemaName, opts) && testEqualNames(lft.pureName, rgt.pureName, opts); +} + +function testEqualsColumns( + a: ColumnInfo, + b: ColumnInfo, + checkName: boolean, + checkDefault: boolean, + opts: DbDiffOptions +) { + if (checkName && !testEqualNames(a.columnName, b.columnName, opts)) { + // opts.DiffLogger.Trace("Column, different name: {0}; {1}", a, b); + return false; + } + //if (!DbDiffTool.EqualFullNames(a.Domain, b.Domain, opts)) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different domain: {2}; {3}", a, b, a.Domain, b.Domain); + // return false; + //} + if (a.computedExpression != b.computedExpression) { + // opts.DiffLogger.Trace( + // 'Column {0}, {1}: different computed expression: {2}; {3}', + // a, + // b, + // a.ComputedExpression, + // b.ComputedExpression + // ); + return false; + } + if (a.computedExpression != null) { + return true; + } + if (checkDefault) { + if (a.defaultValue == null) { + if (a.defaultValue != b.defaultValue) { + // opts.DiffLogger.Trace( + // 'Column {0}, {1}: different default values: {2}; {3}', + // a, + // b, + // a.DefaultValue, + // b.DefaultValue + // ); + return false; + } + } else { + if (a.defaultValue != b.defaultValue) { + // opts.DiffLogger.Trace( + // 'Column {0}, {1}: different default values: {2}; {3}', + // a, + // b, + // a.DefaultValue, + // b.DefaultValue + // ); + return false; + } + } + if (a.defaultConstraint != b.defaultConstraint) { + // opts.DiffLogger.Trace( + // 'Column {0}, {1}: different default constraint names: {2}; {3}', + // a, + // b, + // a.DefaultConstraint, + // b.DefaultConstraint + // ); + return false; + } + } + if (a.notNull != b.notNull) { + // opts.DiffLogger.Trace('Column {0}, {1}: different nullable: {2}; {3}', a, b, a.NotNull, b.NotNull); + return false; + } + if (a.autoIncrement != b.autoIncrement) { + // opts.DiffLogger.Trace('Column {0}, {1}: different autoincrement: {2}; {3}', a, b, a.AutoIncrement, b.AutoIncrement); + return false; + } + if (a.isSparse != b.isSparse) { + // opts.DiffLogger.Trace('Column {0}, {1}: different is_sparse: {2}; {3}', a, b, a.IsSparse, b.IsSparse); + return false; + } + + if (!testEqualTypes(a, b, opts)) { + return false; + } + + //var btype = b.DataType; + //var atype = a.DataType; + //if (pairing != null && pairing.Target != null && pairing.Source.Dialect != null) + //{ + // btype = pairing.Source.Dialect.MigrateDataType(b, btype, pairing.Source.Dialect.GetDefaultMigrationProfile(), null); + // btype = pairing.Source.Dialect.GenericTypeToSpecific(btype).ToGenericType(); + + // // normalize type + // atype = pairing.Source.Dialect.GenericTypeToSpecific(atype).ToGenericType(); + //} + //if (!EqualTypes(atype, btype, opts)) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different types: {2}; {3}", a, b, a.DataType, b.DataType); + // return false; + //} + //if (!opts.IgnoreColumnCollation && a.Collation != b.Collation) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different collations: {2}; {3}", a, b, a.Collation, b.Collation); + // return false; + //} + //if (!opts.IgnoreColumnCharacterSet && a.CharacterSet != b.CharacterSet) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different character sets: {2}; {3}", a, b, a.CharacterSet, b.CharacterSet); + // return false; + //} + return true; +} + +function testEqualTypes(a: ColumnInfo, b: ColumnInfo, opts: DbDiffOptions) { + if (a.dataType != b.dataType) { + // opts.DiffLogger.Trace("Column {0}, {1}: different types: {2}; {3}", a, b, a.DataType, b.DataType); + return false; + } + + //if (a.Length != b.Length) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different lengths: {2}; {3}", a, b, a.Length, b.Length); + // return false; + //} + + //if (a.Precision != b.Precision) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different lengths: {2}; {3}", a, b, a.Precision, b.Precision); + // return false; + //} + + //if (a.Scale != b.Scale) + //{ + // opts.DiffLogger.Trace("Column {0}, {1}: different scale: {2}; {3}", a, b, a.Scale, b.Scale); + // return false; + //} + + return true; +} + function getTableConstraints(table: TableInfo) { const res = []; if (table.primaryKey) res.push(table.primaryKey); @@ -63,30 +224,61 @@ function createPairs(oldList, newList, additionalCondition = null) { return res; } -function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInfo, options: DbDiffOptions) { +function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInfo, opts: DbDiffOptions) { // if (oldTable.primaryKey) + const columnPairs = createPairs(oldTable.columns, newTable.columns); const constraintPairs = createPairs( getTableConstraints(oldTable), getTableConstraints(newTable), (a, b) => a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey' ); - const columnPairs = createPairs(oldTable.columns, newTable.columns); - constraintPairs.filter(x => x[1] == null).forEach(x => plan.dropConstraint(x)); + 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 (!testEqualFullNames(oldTable, newTable, opts)) { + plan.renameTable(oldTable, newTable.pureName); + } + + columnPairs.filter(x => x[0] == null).forEach(x => plan.createColumn(x[1])); + + columnPairs + .filter(x => x[0] && x[1]) + .forEach(x => { + if (!testEqualsColumns(x[0], x[1], true, true, opts)) { + if (!testEqualsColumns(x[0], x[1], false, true, opts)) { + plan.renameColumn(x[0], x[1].columnName); + } else { + plan.changeColumn(x[0], x[1]); + } + } + }); + + constraintPairs.filter(x => x[0] == null).forEach(x => plan.createConstraint(x[1])); } export function createAlterTablePlan( oldTable: TableInfo, newTable: TableInfo, - options: DbDiffOptions, + opts: DbDiffOptions, db: DatabaseInfo ): AlterPlan { const plan = new AlterPlan(db); if (oldTable == null) { plan.createTable(newTable); } else { - planAlterTable(plan, oldTable, newTable, options); + planAlterTable(plan, oldTable, newTable, opts); } return plan; } + +export function getAlterTableScript( + oldTable: TableInfo, + newTable: TableInfo, + opts: DbDiffOptions, + db: DatabaseInfo, + driver: EngineDriver +): string { + const plan = createAlterTablePlan(oldTable, newTable, opts, db); +} diff --git a/packages/types/alter-processor.d.ts b/packages/types/alter-processor.d.ts new file mode 100644 index 000000000..af939abf4 --- /dev/null +++ b/packages/types/alter-processor.d.ts @@ -0,0 +1,15 @@ +import { ColumnInfo, ConstraintInfo, TableInfo } from './dbinfo'; + +export interface AlterProcessor { + createTable(table: TableInfo); + dropTable(table: TableInfo); + createColumn(column: ColumnInfo, constraints: ConstraintInfo[]); + changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo); + dropColumn(column: ColumnInfo); + createConstraint(constraint: ConstraintInfo); + changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo); + dropConstraint(constraint: ConstraintInfo); + renameTable(table: TableInfo, newName: string); + renameColumn(column: ColumnInfo, newName: string); + renameConstraint(constraint: ConstraintInfo, newName: string); +} diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 3ff9e1cd4..aed04c88e 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -42,3 +42,4 @@ export * from './dialect'; export * from './dumper'; export * from './dbtypes'; export * from './extensions'; +export * from './alter-processor';