improved modification detection algorithm - for mssql

This commit is contained in:
Jan Prochazka 2021-05-15 21:14:00 +02:00
parent 2eb1c04fcf
commit cf5afb43eb
3 changed files with 100 additions and 66 deletions

View File

@ -2,6 +2,7 @@ import { DatabaseInfo, DatabaseModification, EngineDriver } from 'dbgate-types';
import _sortBy from 'lodash/sortBy'; import _sortBy from 'lodash/sortBy';
import _groupBy from 'lodash/groupBy'; import _groupBy from 'lodash/groupBy';
import _pick from 'lodash/pick'; import _pick from 'lodash/pick';
import _ from 'lodash';
const fp_pick = arg => array => _pick(array, arg); const fp_pick = arg => array => _pick(array, arg);
export class DatabaseAnalyser { export class DatabaseAnalyser {
@ -16,17 +17,15 @@ export class DatabaseAnalyser {
return DatabaseAnalyser.createEmptyStructure(); return DatabaseAnalyser.createEmptyStructure();
} }
async _computeSingleObjectId() {} async _getFastSnapshot(): Promise<DatabaseInfo> {
/** @returns {Promise<import('dbgate-types').DatabaseModification[]>} */
async getModifications() {
if (this.structure == null) throw new Error('DatabaseAnalyse.getModifications - structure must be filled');
return null; return null;
} }
async _computeSingleObjectId() {}
async fullAnalysis() { async fullAnalysis() {
return this._runAnalysis(); const res = await this._runAnalysis();
return res;
} }
async singleObjectAnalysis(name, typeField) { async singleObjectAnalysis(name, typeField) {
@ -49,7 +48,7 @@ export class DatabaseAnalyser {
} }
if (this.modifications.length == 0) return null; if (this.modifications.length == 0) return null;
console.log('DB modifications detected:', this.modifications); console.log('DB modifications detected:', this.modifications);
return this._runAnalysis(); return this.mergeAnalyseResult(await this._runAnalysis());
} }
mergeAnalyseResult(newlyAnalysed) { mergeAnalyseResult(newlyAnalysed) {
@ -69,7 +68,7 @@ export class DatabaseAnalyser {
const addedChangedIds = newArray.map(x => x.objectId); const addedChangedIds = newArray.map(x => x.objectId);
const removeAllIds = [...removedIds, ...addedChangedIds]; const removeAllIds = [...removedIds, ...addedChangedIds];
res[field] = _sortBy( res[field] = _sortBy(
[...this.structure[field].filter(x => !removeAllIds.includes(x.objectId)), ...newArray], [...(this.structure[field] || []).filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
x => x.pureName x => x.pureName
); );
} }
@ -109,6 +108,11 @@ export class DatabaseAnalyser {
if (!this.modifications || !typeFields || this.modifications.length == 0) { if (!this.modifications || !typeFields || this.modifications.length == 0) {
res = res.replace(/=OBJECT_ID_CONDITION/g, ' is not null'); res = res.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
} else { } 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 const filterIds = this.modifications
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change')) .filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
.map(x => x.objectId); .map(x => x.objectId);
@ -121,6 +125,72 @@ export class DatabaseAnalyser {
return res; 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 { static createEmptyStructure(): DatabaseInfo {
return { return {
tables: [], tables: [],

View File

@ -78,6 +78,6 @@ export interface DatabaseModification {
oldName?: NamedObjectInfo; oldName?: NamedObjectInfo;
newName?: NamedObjectInfo; newName?: NamedObjectInfo;
objectId?: string; objectId?: string;
action: 'add' | 'remove' | 'change'; action: 'add' | 'remove' | 'change' | 'all';
objectTypeField: keyof DatabaseInfo; objectTypeField: keyof DatabaseInfo;
} }

View File

@ -88,6 +88,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
const tables = tablesRows.rows.map(row => ({ const tables = tablesRows.rows.map(row => ({
...row, ...row,
contentHash: row.modifyDate.toISOString(),
columns: columnsRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo), columns: columnsRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo),
primaryKey: DatabaseAnalyser.extractPrimaryKeys(row, pkColumnsRows.rows), primaryKey: DatabaseAnalyser.extractPrimaryKeys(row, pkColumnsRows.rows),
foreignKeys: DatabaseAnalyser.extractForeignKeys(row, fkColumnsRows.rows), foreignKeys: DatabaseAnalyser.extractForeignKeys(row, fkColumnsRows.rows),
@ -95,6 +96,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
const views = viewsRows.rows.map(row => ({ const views = viewsRows.rows.map(row => ({
...row, ...row,
contentHash: row.modifyDate.toISOString(),
createSql: getCreateSql(row), createSql: getCreateSql(row),
columns: viewColumnRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo), columns: viewColumnRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo),
})); }));
@ -103,6 +105,7 @@ class MsSqlAnalyser extends DatabaseAnalyser {
.filter(x => x.sqlObjectType.trim() == 'P') .filter(x => x.sqlObjectType.trim() == 'P')
.map(row => ({ .map(row => ({
...row, ...row,
contentHash: row.modifyDate.toISOString(),
createSql: getCreateSql(row), createSql: getCreateSql(row),
})); }));
@ -110,75 +113,36 @@ class MsSqlAnalyser extends DatabaseAnalyser {
.filter(x => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim())) .filter(x => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim()))
.map(row => ({ .map(row => ({
...row, ...row,
contentHash: row.modifyDate.toISOString(),
createSql: getCreateSql(row), createSql: getCreateSql(row),
})); }));
return this.mergeAnalyseResult({ return {
tables, tables,
views, views,
procedures, procedures,
functions, functions,
schemas, schemas,
});
}
getDeletedObjectsForField(idArray, objectTypeField) {
return this.structure[objectTypeField]
.filter(x => !idArray.includes(x.objectId))
.map(x => ({
oldName: _.pick(x, ['schemaName', 'pureName']),
objectId: x.objectId,
action: 'remove',
objectTypeField,
}));
}
getDeletedObjects(idArray) {
return [
...this.getDeletedObjectsForField(idArray, 'tables'),
...this.getDeletedObjectsForField(idArray, 'views'),
...this.getDeletedObjectsForField(idArray, 'procedures'),
...this.getDeletedObjectsForField(idArray, 'functions'),
...this.getDeletedObjectsForField(idArray, 'triggers'),
];
}
async getModifications() {
const modificationsQueryData = await this.driver.query(this.pool, this.createQuery('modifications'));
// console.log('MOD - SRC', modifications);
// console.log(
// 'MODs',
// this.structure.tables.map((x) => x.modifyDate)
// );
const modifications = modificationsQueryData.rows.map(x => {
const { type, objectId, modifyDate, schemaName, pureName } = x;
const field = objectTypeToField(type);
if (!this.structure[field]) return null;
// @ts-ignore
const obj = this.structure[field].find(x => x.objectId == objectId);
// object not modified
if (obj && Math.abs(new Date(modifyDate).getTime() - new Date(obj.modifyDate).getTime()) < 1000) return null;
/** @type {import('dbgate-types').DatabaseModification} */
const action = obj
? {
newName: { schemaName, pureName },
oldName: _.pick(obj, ['schemaName', 'pureName']),
action: 'change',
objectTypeField: field,
objectId,
}
: {
newName: { schemaName, pureName },
action: 'add',
objectTypeField: field,
objectId,
}; };
return action; }
});
return [..._.compact(modifications), ...this.getDeletedObjects(modificationsQueryData.rows.map(x => x.objectId))]; async _getFastSnapshot() {
const modificationsQueryData = await this.driver.query(this.pool, this.createQuery('modifications'));
const res = DatabaseAnalyser.createEmptyStructure();
for (const item of modificationsQueryData.rows) {
const { type, objectId, modifyDate, schemaName, pureName } = item;
const field = objectTypeToField(type);
if (!field || !res[field]) continue;
res[field].push({
objectId,
contentHash: modifyDate.toISOString(),
schemaName,
pureName,
});
}
return res;
} }
} }