mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 22:36:01 +00:00
321 lines
8.7 KiB
TypeScript
321 lines
8.7 KiB
TypeScript
import type {
|
|
DatabaseInfo,
|
|
EngineDriver,
|
|
FunctionInfo,
|
|
ProcedureInfo,
|
|
SchedulerEventInfo,
|
|
TableInfo,
|
|
TriggerInfo,
|
|
ViewInfo,
|
|
} from 'dbgate-types';
|
|
import _flatten from 'lodash/flatten';
|
|
import _uniqBy from 'lodash/uniqBy';
|
|
import { getLogger } from './getLogger';
|
|
import { SqlDumper } from './SqlDumper';
|
|
import { extendDatabaseInfo } from './structureTools';
|
|
import { extractErrorLogData } from './stringTools';
|
|
|
|
const logger = getLogger('sqlGenerator');
|
|
|
|
interface SqlGeneratorOptions {
|
|
dropTables: boolean;
|
|
checkIfTableExists: boolean;
|
|
dropReferences: boolean;
|
|
createTables: boolean;
|
|
createReferences: boolean;
|
|
createForeignKeys: boolean;
|
|
createIndexes: boolean;
|
|
insert: boolean;
|
|
skipAutoincrementColumn: boolean;
|
|
disableConstraints: boolean;
|
|
omitNulls: boolean;
|
|
truncate: boolean;
|
|
|
|
dropViews: boolean;
|
|
checkIfViewExists: boolean;
|
|
createViews: boolean;
|
|
|
|
dropMatviews: boolean;
|
|
checkIfMatviewExists: boolean;
|
|
createMatviews: boolean;
|
|
|
|
dropProcedures: boolean;
|
|
checkIfProcedureExists: boolean;
|
|
createProcedures: boolean;
|
|
|
|
dropFunctions: boolean;
|
|
checkIfFunctionExists: boolean;
|
|
createFunctions: boolean;
|
|
|
|
dropTriggers: boolean;
|
|
checkIfTriggerExists: boolean;
|
|
createTriggers: boolean;
|
|
|
|
dropSchedulerEvents: boolean;
|
|
checkIfSchedulerEventExists: boolean;
|
|
createSchedulerEvents: boolean;
|
|
}
|
|
|
|
interface SqlGeneratorObject {
|
|
schemaName: string;
|
|
pureName: string;
|
|
objectTypeField: 'tables' | 'views' | 'procedures' | 'functions' | 'triggers' | 'schedulerEvents';
|
|
}
|
|
|
|
export class SqlGenerator {
|
|
private tables: TableInfo[];
|
|
private views: ViewInfo[];
|
|
private matviews: ViewInfo[];
|
|
private procedures: ProcedureInfo[];
|
|
private functions: FunctionInfo[];
|
|
private triggers: TriggerInfo[];
|
|
private schedulerEvents: SchedulerEventInfo[];
|
|
public dbinfo: DatabaseInfo;
|
|
public isTruncated = false;
|
|
public isUnhandledException = false;
|
|
|
|
constructor(
|
|
dbinfo: DatabaseInfo,
|
|
public options: SqlGeneratorOptions,
|
|
public objects: SqlGeneratorObject[],
|
|
public dmp: SqlDumper,
|
|
public driver: EngineDriver,
|
|
public pool
|
|
) {
|
|
this.dbinfo = extendDatabaseInfo(dbinfo);
|
|
this.tables = this.extract('tables');
|
|
this.views = this.extract('views');
|
|
this.matviews = this.extract('matviews');
|
|
this.procedures = this.extract('procedures');
|
|
this.functions = this.extract('functions');
|
|
this.triggers = this.extract('triggers');
|
|
this.schedulerEvents = this.extract('schedulerEvents');
|
|
}
|
|
|
|
private handleException = error => {
|
|
logger.error(extractErrorLogData(error), 'Unhandled error');
|
|
this.isUnhandledException = true;
|
|
};
|
|
|
|
async dump() {
|
|
try {
|
|
process.on('uncaughtException', this.handleException);
|
|
|
|
this.dropObjects(this.procedures, 'Procedure');
|
|
if (this.checkDumper()) return;
|
|
this.dropObjects(this.functions, 'Function');
|
|
if (this.checkDumper()) return;
|
|
this.dropObjects(this.views, 'View');
|
|
if (this.checkDumper()) return;
|
|
this.dropObjects(this.matviews, 'Matview');
|
|
if (this.checkDumper()) return;
|
|
this.dropObjects(this.triggers, 'Trigger');
|
|
if (this.checkDumper()) return;
|
|
this.dropObjects(this.schedulerEvents, 'SchedulerEvent');
|
|
if (this.checkDumper()) return;
|
|
|
|
this.dropTables();
|
|
if (this.checkDumper()) return;
|
|
|
|
this.createTables();
|
|
if (this.checkDumper()) return;
|
|
|
|
this.truncateTables();
|
|
if (this.checkDumper()) return;
|
|
|
|
await this.insertData();
|
|
if (this.checkDumper()) return;
|
|
|
|
this.createForeignKeys();
|
|
if (this.checkDumper()) return;
|
|
|
|
this.createObjects(this.procedures, 'Procedure');
|
|
if (this.checkDumper()) return;
|
|
this.createObjects(this.functions, 'Function');
|
|
if (this.checkDumper()) return;
|
|
this.createObjects(this.views, 'View');
|
|
if (this.checkDumper()) return;
|
|
this.createObjects(this.matviews, 'Matview');
|
|
if (this.checkDumper()) return;
|
|
this.createObjects(this.triggers, 'Trigger');
|
|
if (this.checkDumper()) return;
|
|
this.createObjects(this.schedulerEvents, 'SchedulerEvent');
|
|
if (this.checkDumper()) return;
|
|
} finally {
|
|
process.off('uncaughtException', this.handleException);
|
|
}
|
|
}
|
|
|
|
createForeignKeys() {
|
|
const fks = [];
|
|
if (this.options.createForeignKeys) fks.push(..._flatten(this.tables.map(x => x.foreignKeys || [])));
|
|
if (this.options.createReferences) fks.push(..._flatten(this.tables.map(x => x.dependencies || [])));
|
|
for (const fk of _uniqBy(fks, 'constraintName')) {
|
|
this.dmp.createForeignKey(fk);
|
|
if (this.checkDumper()) return;
|
|
}
|
|
}
|
|
|
|
truncateTables() {
|
|
if (this.options.truncate) {
|
|
for (const table of this.tables) {
|
|
this.dmp.truncateTable(table);
|
|
if (this.checkDumper()) return;
|
|
}
|
|
}
|
|
}
|
|
|
|
createTables() {
|
|
if (this.options.createTables) {
|
|
for (const table of this.tables) {
|
|
this.dmp.createTable({
|
|
...table,
|
|
foreignKeys: [],
|
|
dependencies: [],
|
|
indexes: [],
|
|
});
|
|
if (this.checkDumper()) return;
|
|
}
|
|
}
|
|
if (this.options.createIndexes) {
|
|
for (const index of _flatten(this.tables.map(x => x.indexes || []))) {
|
|
this.dmp.createIndex(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
async insertData() {
|
|
if (!this.options.insert) return;
|
|
|
|
this.enableConstraints(false);
|
|
|
|
for (const table of this.tables) {
|
|
await this.insertTableData(table);
|
|
if (this.checkDumper()) return;
|
|
}
|
|
|
|
this.enableConstraints(true);
|
|
}
|
|
|
|
checkDumper() {
|
|
if (this.dmp.s.length > 4000000) {
|
|
if (!this.isTruncated) {
|
|
this.dmp.putRaw('\n');
|
|
this.dmp.comment(' *************** SQL is truncated ******************');
|
|
this.dmp.putRaw('\n');
|
|
}
|
|
this.isTruncated = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
dropObjects(list, name) {
|
|
if (this.options[`drop${name}s`]) {
|
|
for (const item of list) {
|
|
this.dmp[`drop${name}`](item, { testIfExists: this.options[`checkIf${name}Exists`] });
|
|
if (this.checkDumper()) return;
|
|
}
|
|
}
|
|
}
|
|
|
|
createObjects(list, name) {
|
|
if (this.options[`create${name}s`]) {
|
|
for (const item of list) {
|
|
this.dmp[`create${name}`](item);
|
|
if (this.checkDumper()) return;
|
|
}
|
|
}
|
|
}
|
|
|
|
dropTables() {
|
|
if (this.options.dropReferences) {
|
|
for (const fk of _flatten(this.tables.map(x => x.dependencies || []))) {
|
|
this.dmp.dropForeignKey(fk);
|
|
}
|
|
}
|
|
|
|
if (this.options.dropTables) {
|
|
for (const table of this.tables) {
|
|
this.dmp.dropTable(table, { testIfExists: this.options.checkIfTableExists });
|
|
}
|
|
}
|
|
}
|
|
|
|
async insertTableData(table: TableInfo) {
|
|
const dmpLocal = this.driver.createDumper();
|
|
dmpLocal.put('^select * ^from %f', table);
|
|
|
|
const autoinc = table.columns.find(x => x.autoIncrement);
|
|
if (autoinc && !this.options.skipAutoincrementColumn) {
|
|
this.dmp.allowIdentityInsert(table, true);
|
|
}
|
|
|
|
const readable = await this.driver.readQuery(this.pool, dmpLocal.s, table);
|
|
await this.processReadable(table, readable);
|
|
|
|
if (autoinc && !this.options.skipAutoincrementColumn) {
|
|
this.dmp.allowIdentityInsert(table, false);
|
|
}
|
|
}
|
|
|
|
processReadable(table: TableInfo, readable) {
|
|
const columnsFiltered = this.options.skipAutoincrementColumn
|
|
? table.columns.filter(x => !x.autoIncrement)
|
|
: table.columns;
|
|
const columnNames = columnsFiltered.map(x => x.columnName);
|
|
let isClosed = false;
|
|
let isHeaderRead = false;
|
|
|
|
return new Promise(resolve => {
|
|
readable.on('data', chunk => {
|
|
if (isClosed) return;
|
|
if (!isHeaderRead) {
|
|
isHeaderRead = true;
|
|
return;
|
|
}
|
|
|
|
if (this.checkDumper()) {
|
|
isClosed = true;
|
|
resolve(undefined);
|
|
readable.destroy();
|
|
return;
|
|
}
|
|
|
|
const columnNamesCopy = this.options.omitNulls ? columnNames.filter(col => chunk[col] != null) : columnNames;
|
|
this.dmp.put(
|
|
'^insert ^into %f (%,i) ^values (%,v);&n',
|
|
table,
|
|
columnNamesCopy,
|
|
columnNamesCopy.map(col => chunk[col])
|
|
);
|
|
});
|
|
readable.on('end', () => {
|
|
resolve(undefined);
|
|
});
|
|
});
|
|
}
|
|
|
|
extract(objectTypeField) {
|
|
return (
|
|
this.dbinfo[objectTypeField]?.filter(x =>
|
|
this.objects.find(
|
|
y => x.pureName == y.pureName && x.schemaName == y.schemaName && y.objectTypeField == objectTypeField
|
|
)
|
|
) ?? []
|
|
);
|
|
}
|
|
|
|
enableConstraints(enabled) {
|
|
if (this.options.disableConstraints) {
|
|
if (this.driver.dialect.enableConstraintsPerTable) {
|
|
for (const table of this.tables) {
|
|
this.dmp.enableConstraints(table, enabled);
|
|
}
|
|
} else {
|
|
this.dmp.enableConstraints(null, enabled);
|
|
}
|
|
}
|
|
}
|
|
}
|