Files
dbgate/packages/tools/src/DatabaseAnalyser.ts
2021-05-15 21:30:12 +02:00

237 lines
7.4 KiB
TypeScript

import { DatabaseInfo, DatabaseModification, EngineDriver } from 'dbgate-types';
import _sortBy from 'lodash/sortBy';
import _groupBy from 'lodash/groupBy';
import _pick from 'lodash/pick';
import _ from 'lodash';
const fp_pick = arg => array => _pick(array, arg);
export class DatabaseAnalyser {
structure: DatabaseInfo;
modifications: DatabaseModification[];
singleObjectFilter: any;
singleObjectId: string = null;
constructor(public pool, public driver: EngineDriver) {}
async _runAnalysis() {
return DatabaseAnalyser.createEmptyStructure();
}
async _getFastSnapshot(): Promise<DatabaseInfo> {
return null;
}
async _computeSingleObjectId() {}
async fullAnalysis() {
const res = await this._runAnalysis();
return res;
}
async singleObjectAnalysis(name, typeField) {
this.singleObjectFilter = { ...name, typeField };
await this._computeSingleObjectId();
const res = this._runAnalysis();
if (res[typeField].length == 1) return res[typeField][0];
const obj = res[typeField].find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
return obj;
}
async incrementalAnalysis(structure) {
this.structure = structure;
this.modifications = await this.getModifications();
if (this.modifications == null) {
// modifications not implemented, perform full analysis
this.structure = null;
return this._runAnalysis();
}
if (this.modifications.length == 0) return null;
console.log('DB modifications detected:', this.modifications);
return this.mergeAnalyseResult(await this._runAnalysis());
}
mergeAnalyseResult(newlyAnalysed) {
if (this.structure == null) {
return {
...DatabaseAnalyser.createEmptyStructure(),
...newlyAnalysed,
};
}
const res = {};
for (const field of ['tables', 'collections', 'views', 'functions', 'procedures', 'triggers']) {
const removedIds = this.modifications
.filter(x => x.action == 'remove' && x.objectTypeField == field)
.map(x => x.objectId);
const newArray = newlyAnalysed[field] || [];
const addedChangedIds = newArray.map(x => x.objectId);
const removeAllIds = [...removedIds, ...addedChangedIds];
res[field] = _sortBy(
[...(this.structure[field] || []).filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
x => x.pureName
);
}
return res;
// const {tables,views, functions, procedures, triggers} = this.structure;
// return {
// tables:
// }
}
getRequestedObjectPureNames(objectTypeField, allPureNames) {
if (this.singleObjectFilter) {
const { typeField, pureName } = this.singleObjectFilter;
if (typeField == objectTypeField) return [pureName];
}
if (this.modifications) {
return this.modifications.filter(x => x.objectTypeField == objectTypeField).map(x => x.newName.pureName);
}
return allPureNames;
}
// findObjectById(id) {
// return this.structure.tables.find((x) => x.objectId == id);
// }
createQuery(template, typeFields) {
let res = template;
if (this.singleObjectFilter) {
const { typeField } = this.singleObjectFilter;
if (!this.singleObjectId) return null;
if (!typeFields || !typeFields.includes(typeField)) return null;
return res.replace(/=OBJECT_ID_CONDITION/g, ` = ${this.singleObjectId}`);
}
if (!this.modifications || !typeFields || this.modifications.length == 0) {
res = res.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
} else {
if (this.modifications.some(x => typeFields.includes(x.objectTypeField) && x.action == 'all')) {
// do not filter objects
res = res.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
}
const filterIds = this.modifications
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
.map(x => x.objectId);
if (filterIds.length == 0) {
res = res.replace(/=OBJECT_ID_CONDITION/g, ' = 0');
} else {
res = res.replace(/=OBJECT_ID_CONDITION/g, ` in (${filterIds.map(x => `'${x}'`).join(',')})`);
}
}
return res;
}
getDeletedObjectsForField(snapshot, objectTypeField) {
const items = snapshot[objectTypeField];
if (!items) return [];
if (!this.structure[objectTypeField]) return [];
return this.structure[objectTypeField]
.filter(x => !items.find(y => x.objectId == y.objectId))
.map(x => ({
oldName: _.pick(x, ['schemaName', 'pureName']),
objectId: x.objectId,
action: 'remove',
objectTypeField,
}));
}
getDeletedObjects(snapshot) {
return [
...this.getDeletedObjectsForField(snapshot, 'tables'),
...this.getDeletedObjectsForField(snapshot, 'collections'),
...this.getDeletedObjectsForField(snapshot, 'views'),
...this.getDeletedObjectsForField(snapshot, 'procedures'),
...this.getDeletedObjectsForField(snapshot, 'functions'),
...this.getDeletedObjectsForField(snapshot, 'triggers'),
];
}
async getModifications() {
const snapshot = await this._getFastSnapshot();
if (!snapshot) return null;
console.log('STRUCTURE', this.structure);
console.log('SNAPSHOT', snapshot);
const res = [];
for (const field in snapshot) {
const items = snapshot[field];
if (items === null) {
res.push({ objectTypeField: field, action: 'all' });
continue;
}
for (const item of items) {
const { objectId, schemaName, pureName, contentHash } = item;
const obj = this.structure[field].find(x => x.objectId == objectId);
if (obj && contentHash && obj.contentHash == contentHash) continue;
const action = obj
? {
newName: { schemaName, pureName },
oldName: _.pick(obj, ['schemaName', 'pureName']),
action: 'change',
objectTypeField: field,
objectId,
}
: {
newName: { schemaName, pureName },
action: 'add',
objectTypeField: field,
objectId,
};
res.push(action);
}
return [..._.compact(res), ...this.getDeletedObjects(snapshot)];
}
}
static createEmptyStructure(): DatabaseInfo {
return {
tables: [],
collections: [],
views: [],
functions: [],
procedures: [],
triggers: [],
schemas: [],
};
}
static byTableFilter(table) {
return x => x.pureName == table.pureName && x.schemaName == x.schemaName;
}
static extractPrimaryKeys(table, pkColumns) {
const filtered = pkColumns.filter(DatabaseAnalyser.byTableFilter(table));
if (filtered.length == 0) return undefined;
return {
..._pick(filtered[0], ['constraintName', 'schemaName', 'pureName']),
constraintType: 'primaryKey',
columns: filtered.map(fp_pick('columnName')),
};
}
static extractForeignKeys(table, fkColumns) {
const grouped = _groupBy(fkColumns.filter(DatabaseAnalyser.byTableFilter(table)), 'constraintName');
return Object.keys(grouped).map(constraintName => ({
constraintName,
constraintType: 'foreignKey',
..._pick(grouped[constraintName][0], [
'constraintName',
'schemaName',
'pureName',
'refSchemaName',
'refTableName',
'updateAction',
'deleteAction',
]),
columns: grouped[constraintName].map(fp_pick(['columnName', 'refColumnName'])),
}));
}
}