recreate table WIP

This commit is contained in:
Jan Prochazka 2021-08-26 16:29:28 +02:00
parent dfe37496f2
commit 3bbe06a55b
10 changed files with 222 additions and 12 deletions

View File

@ -55,7 +55,8 @@ async function testTableDiff(conn, driver, mangle) {
// expect(stableStringify(structure2)).toEqual(stableStringify(structure2Real)); // expect(stableStringify(structure2)).toEqual(stableStringify(structure2Real));
} }
const TESTED_COLUMNS = ['col_pk', 'col_std', 'col_def', 'col_fk', 'col_ref', 'col_idx', 'col_uq']; // const TESTED_COLUMNS = ['col_pk', 'col_std', 'col_def', 'col_fk', 'col_ref', 'col_idx', 'col_uq'];
const TESTED_COLUMNS = ['col_pk'];
// const TESTED_COLUMNS = ['col_idx']; // const TESTED_COLUMNS = ['col_idx'];
// const TESTED_COLUMNS = ['col_fk']; // const TESTED_COLUMNS = ['col_fk'];
// const TESTED_COLUMNS = ['col_std']; // const TESTED_COLUMNS = ['col_std'];

View File

@ -20,6 +20,7 @@ import {
import _isString from 'lodash/isString'; import _isString from 'lodash/isString';
import _isNumber from 'lodash/isNumber'; import _isNumber from 'lodash/isNumber';
import _isDate from 'lodash/isDate'; import _isDate from 'lodash/isDate';
import uuidv1 from 'uuid/v1';
export class SqlDumper implements AlterProcessor { export class SqlDumper implements AlterProcessor {
s = ''; s = '';
@ -372,6 +373,25 @@ export class SqlDumper implements AlterProcessor {
break; break;
} }
} }
createConstraint(cnt: ConstraintInfo) {
switch (cnt.constraintType) {
case 'primaryKey':
this.createPrimaryKey(cnt as PrimaryKeyInfo);
break;
case 'foreignKey':
this.createForeignKey(cnt as ForeignKeyInfo);
break;
case 'unique':
this.createUnique(cnt as UniqueInfo);
break;
case 'check':
this.createCheck(cnt as CheckInfo);
break;
case 'index':
this.createIndex(cnt as IndexInfo);
break;
}
}
dropForeignKey(fk: ForeignKeyInfo) { dropForeignKey(fk: ForeignKeyInfo) {
if (this.dialect.explicitDropConstraint) { if (this.dialect.explicitDropConstraint) {
this.putCmd('^alter ^table %f ^drop ^foreign ^key %i', fk, fk.constraintName); this.putCmd('^alter ^table %f ^drop ^foreign ^key %i', fk, fk.constraintName);
@ -463,7 +483,7 @@ export class SqlDumper implements AlterProcessor {
changeColumn(oldcol: ColumnInfo, newcol: ColumnInfo, constraints: ConstraintInfo[]) {} changeColumn(oldcol: ColumnInfo, newcol: ColumnInfo, constraints: ConstraintInfo[]) {}
dropTable(obj: TableInfo, { testIfExists = false }) { dropTable(obj: TableInfo, { testIfExists = false } = {}) {
this.putCmd('^drop ^table %f', obj); this.putCmd('^drop ^table %f', obj);
} }
@ -489,4 +509,61 @@ export class SqlDumper implements AlterProcessor {
truncateTable(name: NamedObjectInfo) { truncateTable(name: NamedObjectInfo) {
this.putCmd('^delete ^from %f', name); this.putCmd('^delete ^from %f', name);
} }
dropConstraints(table: TableInfo, dropReferences = false) {
if (dropReferences && this.dialect.dropForeignKey) {
table.dependencies.forEach(cnt => this.dropConstraint(cnt));
}
if (this.dialect.dropIndex) {
table.indexes.forEach(cnt => this.dropIndex(cnt));
}
if (this.dialect.dropForeignKey) {
table.foreignKeys.forEach(cnt => this.dropForeignKey(cnt));
}
if (this.dialect.dropPrimaryKey && table.primaryKey) {
this.dropPrimaryKey(table.primaryKey);
}
}
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
if (oldTable.pairingId != newTable.pairingId) {
throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId');
}
const tmpTable = `temp_${uuidv1()}`;
const columnPairs = oldTable.columns
.map(oldcol => ({
oldcol,
newcol: newTable.columns.find(x => x.pairingId == oldcol.pairingId),
}))
.filter(x => x.newcol);
this.dropConstraints(oldTable, true);
this.renameTable(oldTable, tmpTable);
this.createTable(newTable);
const autoinc = newTable.columns.find(x => x.autoIncrement);
if (autoinc) {
this.allowIdentityInsert(newTable, true);
}
this.putCmd(
'^insert ^into %f (%,i) select %,s ^from %f',
newTable,
columnPairs.map(x => x.newcol.columnName),
columnPairs.map(x => x.oldcol.columnName),
{ ...oldTable, pureName: tmpTable }
);
if (autoinc) {
this.allowIdentityInsert(newTable, false);
}
if (this.dialect.dropForeignKey) {
newTable.dependencies.forEach(cnt => this.createConstraint(cnt));
}
this.dropTable({ ...oldTable, pureName: tmpTable });
}
} }

View File

@ -8,6 +8,8 @@ import {
SqlDialect, SqlDialect,
TableInfo, TableInfo,
} from '../../types'; } from '../../types';
import { DatabaseInfoAlterProcessor } from './database-info-alter-processor';
import { DatabaseAnalyser } from './DatabaseAnalyser';
interface AlterOperation_CreateTable { interface AlterOperation_CreateTable {
operationType: 'createTable'; operationType: 'createTable';
@ -68,6 +70,11 @@ interface AlterOperation_RenameConstraint {
object: ConstraintInfo; object: ConstraintInfo;
newName: string; newName: string;
} }
interface AlterOperation_RecreateTable {
operationType: 'recreateTable';
table: TableInfo;
operations: AlterOperation[];
}
type AlterOperation = type AlterOperation =
| AlterOperation_CreateColumn | AlterOperation_CreateColumn
@ -80,7 +87,8 @@ type AlterOperation =
| AlterOperation_DropTable | AlterOperation_DropTable
| AlterOperation_RenameTable | AlterOperation_RenameTable
| AlterOperation_RenameColumn | AlterOperation_RenameColumn
| AlterOperation_RenameConstraint; | AlterOperation_RenameConstraint
| AlterOperation_RecreateTable;
export class AlterPlan { export class AlterPlan {
public operations: AlterOperation[] = []; public operations: AlterOperation[] = [];
@ -168,6 +176,14 @@ export class AlterPlan {
}); });
} }
recreateTable(table: TableInfo, operations: AlterOperation[]) {
this.operations.push({
operationType: 'recreateTable',
table,
operations,
});
}
run(processor: AlterProcessor) { run(processor: AlterProcessor) {
for (const op of this.operations) { for (const op of this.operations) {
runAlterOperation(op, processor); runAlterOperation(op, processor);
@ -180,15 +196,15 @@ export class AlterPlan {
const table = this.db.tables.find( const table = this.db.tables.find(
x => x.pureName == op.oldObject.pureName && x.schemaName == op.oldObject.schemaName x => x.pureName == op.oldObject.pureName && x.schemaName == op.oldObject.schemaName
); );
const deletedFks = this.dialect.dropColumnDependencies?.includes('foreignKey') const deletedFks = this.dialect.dropColumnDependencies?.includes('dependencies')
? table.dependencies.filter(fk => fk.columns.find(col => col.refColumnName == op.oldObject.columnName)) ? table.dependencies.filter(fk => fk.columns.find(col => col.refColumnName == op.oldObject.columnName))
: []; : [];
const deletedConstraints = _.compact([ const deletedConstraints = _.compact([
table.primaryKey, this.dialect.dropColumnDependencies?.includes('primaryKey') ? table.primaryKey : null,
...table.foreignKeys, ...(this.dialect.dropColumnDependencies?.includes('foreignKeys') ? table.foreignKeys : []),
...table.indexes, ...(this.dialect.dropColumnDependencies?.includes('indexes') ? table.indexes : []),
...table.uniques, ...(this.dialect.dropColumnDependencies?.includes('uniques') ? table.uniques : []),
]).filter(cnt => cnt.columns.find(col => col.columnName == op.oldObject.columnName)); ]).filter(cnt => cnt.columns.find(col => col.columnName == op.oldObject.columnName));
const res: AlterOperation[] = [ const res: AlterOperation[] = [
@ -209,8 +225,63 @@ export class AlterPlan {
return _.flatten(lists); return _.flatten(lists);
} }
_transformToImplementedOps(): AlterOperation[] {
const lists = this.operations.map(op => {
return (
this._testTableRecreate(op, 'createColumn', this.dialect.createColumn, 'newObject') ||
this._testTableRecreate(op, 'dropColumn', this.dialect.dropColumn, 'oldObject') ||
this._testTableRecreate(op, 'createConstraint', obj => this._canCreateConstraint(obj), 'newObject') ||
this._testTableRecreate(op, 'dropConstraint', obj => this._canDropConstraint(obj), 'oldObject') || [op]
);
});
return _.flatten(lists);
}
_canCreateConstraint(cnt: ConstraintInfo) {
if (cnt.constraintType == 'primaryKey') return this.dialect.createPrimaryKey;
if (cnt.constraintType == 'foreignKey') return this.dialect.createForeignKey;
if (cnt.constraintType == 'index') return this.dialect.createIndex;
return null;
}
_canDropConstraint(cnt: ConstraintInfo) {
if (cnt.constraintType == 'primaryKey') return this.dialect.dropPrimaryKey;
if (cnt.constraintType == 'foreignKey') return this.dialect.dropForeignKey;
if (cnt.constraintType == 'index') return this.dialect.dropIndex;
return null;
}
_testTableRecreate(
op: AlterOperation,
operationType: string,
isAllowed: boolean | Function,
objectField: string
): AlterOperation[] | null {
if (op.operationType == operationType) {
if (_.isFunction(isAllowed)) {
if (!isAllowed(op[objectField])) return null;
} else {
if (!isAllowed) return null;
}
const table = this.db.tables.find(
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
);
return [
{
operationType: 'recreateTable',
table,
operations: [op],
},
];
}
return null;
}
transformPlan() { transformPlan() {
this.operations = this._addLogicalDependencies(); this.operations = this._addLogicalDependencies();
this.operations = this._transformToImplementedOps();
} }
} }
@ -246,5 +317,14 @@ export function runAlterOperation(op: AlterOperation, processor: AlterProcessor)
case 'renameConstraint': case 'renameConstraint':
processor.renameConstraint(op.object, op.newName); processor.renameConstraint(op.object, op.newName);
break; break;
case 'recreateTable':
{
const newTable = _.cloneDeep(op.table);
const newDb = DatabaseAnalyser.createEmptyStructure();
newDb.tables.push(newTable);
op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
processor.recreateTable(op.table, newTable);
}
break;
} }
} }

View File

@ -66,4 +66,8 @@ export class DatabaseInfoAlterProcessor {
} }
renameConstraint(constraint: ConstraintInfo, newName: string) {} renameConstraint(constraint: ConstraintInfo, newName: string) {}
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
throw new Error('recreateTable not implemented for DatabaseInfoAlterProcessor');
}
} }

View File

@ -12,4 +12,5 @@ export interface AlterProcessor {
renameTable(table: TableInfo, newName: string); renameTable(table: TableInfo, newName: string);
renameColumn(column: ColumnInfo, newName: string); renameColumn(column: ColumnInfo, newName: string);
renameConstraint(constraint: ConstraintInfo, newName: string); renameConstraint(constraint: ConstraintInfo, newName: string);
recreateTable(oldTable: TableInfo, newTable: TableInfo);
} }

View File

@ -10,7 +10,17 @@ export interface SqlDialect {
explicitDropConstraint?: boolean; explicitDropConstraint?: boolean;
anonymousPrimaryKey?: boolean; anonymousPrimaryKey?: boolean;
enableConstraintsPerTable?: boolean; enableConstraintsPerTable?: boolean;
nosql?: boolean; // mongo
dropColumnDependencies?: string[]; dropColumnDependencies?: string[];
changeColumnDependencies?: string[]; changeColumnDependencies?: string[];
nosql?: boolean; // mongo
createColumn?: boolean;
dropColumn?: boolean;
createIndex?: boolean;
dropIndex?: boolean;
createForeignKey?: boolean;
dropForeignKey?: boolean;
createPrimaryKey?: boolean;
dropPrimaryKey?: boolean;
} }

View File

@ -12,12 +12,21 @@ const dialect = {
fallbackDataType: 'nvarchar(max)', fallbackDataType: 'nvarchar(max)',
explicitDropConstraint: false, explicitDropConstraint: false,
enableConstraintsPerTable: true, enableConstraintsPerTable: true,
dropColumnDependencies: ['default', 'foreignKey', 'index'], dropColumnDependencies: ['default', 'dependencies', 'indexes', 'primaryKey'],
changeColumnDependencies: ['index'], changeColumnDependencies: ['indexes'],
anonymousPrimaryKey: false, anonymousPrimaryKey: false,
quoteIdentifier(s) { quoteIdentifier(s) {
return `[${s}]`; return `[${s}]`;
}, },
createColumn: true,
dropColumn: true,
createIndex: true,
dropIndex: true,
createForeignKey: true,
dropForeignKey: true,
createPrimaryKey: true,
dropPrimaryKey: true,
}; };
/** @type {import('dbgate-types').EngineDriver} */ /** @type {import('dbgate-types').EngineDriver} */

View File

@ -13,6 +13,15 @@ const dialect = {
quoteIdentifier(s) { quoteIdentifier(s) {
return '`' + s + '`'; return '`' + s + '`';
}, },
createColumn: true,
dropColumn: true,
createIndex: true,
dropIndex: true,
createForeignKey: true,
dropForeignKey: true,
createPrimaryKey: true,
dropPrimaryKey: true,
}; };
const mysqlDriverBase = { const mysqlDriverBase = {

View File

@ -35,6 +35,15 @@ const postgresDriver = {
dialect: { dialect: {
...dialect, ...dialect,
materializedViews: true, materializedViews: true,
createColumn: true,
dropColumn: true,
createIndex: true,
dropIndex: true,
createForeignKey: true,
dropForeignKey: true,
createPrimaryKey: true,
dropPrimaryKey: true,
}, },
}; };
@ -47,6 +56,7 @@ const cockroachDriver = {
dialect: { dialect: {
...dialect, ...dialect,
materializedViews: true, materializedViews: true,
dropColumnDependencies: ['primaryKey'],
}, },
}; };

View File

@ -17,10 +17,19 @@ const dialect = {
explicitDropConstraint: true, explicitDropConstraint: true,
stringEscapeChar: "'", stringEscapeChar: "'",
fallbackDataType: 'nvarchar(max)', fallbackDataType: 'nvarchar(max)',
dropColumnDependencies: ['index'], dropColumnDependencies: ['index', 'primaryKey'],
quoteIdentifier(s) { quoteIdentifier(s) {
return `[${s}]`; return `[${s}]`;
}, },
createColumn: true,
dropColumn: true,
createIndex: true,
dropIndex: true,
createForeignKey: false,
dropForeignKey: false,
createPrimaryKey: false,
dropPrimaryKey: false,
}; };
/** @type {import('dbgate-types').EngineDriver} */ /** @type {import('dbgate-types').EngineDriver} */