mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
improved modification detection algorithm - for mssql
This commit is contained in:
parent
2eb1c04fcf
commit
cf5afb43eb
@ -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: [],
|
||||||
|
2
packages/types/engines.d.ts
vendored
2
packages/types/engines.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user