mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-18 02:06:01 +00:00
Merge branch 'feature/restore-script'
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { SqlDumper } from 'dbgate-types';
|
import type { SqlDumper } from 'dbgate-types';
|
||||||
import { Command, Select, Update, Delete, Insert } from './types';
|
import { Command, Select, Update, Delete, Insert } from './types';
|
||||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||||
import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
|
import { dumpSqlFromDefinition, dumpSqlSourceDef, dumpSqlSourceRef } from './dumpSqlSource';
|
||||||
import { dumpSqlCondition } from './dumpSqlCondition';
|
import { dumpSqlCondition } from './dumpSqlCondition';
|
||||||
|
|
||||||
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
||||||
@@ -115,7 +115,10 @@ export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) {
|
|||||||
cmd.fields.map(x => x.targetColumn)
|
cmd.fields.map(x => x.targetColumn)
|
||||||
);
|
);
|
||||||
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
|
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
|
||||||
if (dmp.dialect.requireFromDual) {
|
if (cmd.whereNotExistsSource) {
|
||||||
|
dmp.put(' ^from ');
|
||||||
|
dumpSqlSourceDef(dmp, cmd.whereNotExistsSource);
|
||||||
|
} else if (dmp.dialect.requireFromDual) {
|
||||||
dmp.put(' ^from ^dual ');
|
dmp.put(' ^from ^dual ');
|
||||||
}
|
}
|
||||||
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
|
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||||||
import type { SqlDumper } from 'dbgate-types';
|
import type { SqlDumper } from 'dbgate-types';
|
||||||
import { Expression, ColumnRefExpression } from './types';
|
import { Expression, ColumnRefExpression } from './types';
|
||||||
import { dumpSqlSourceRef } from './dumpSqlSource';
|
import { dumpSqlSourceRef } from './dumpSqlSource';
|
||||||
|
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||||
|
|
||||||
export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||||
switch (expr.exprType) {
|
switch (expr.exprType) {
|
||||||
@@ -67,5 +68,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
|||||||
});
|
});
|
||||||
dmp.put(')');
|
dmp.put(')');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
dmp.put('(');
|
||||||
|
dumpSqlSelect(dmp, expr.select);
|
||||||
|
dmp.put(')');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface Insert {
|
|||||||
fields: UpdateField[];
|
fields: UpdateField[];
|
||||||
targetTable: NamedObjectInfo;
|
targetTable: NamedObjectInfo;
|
||||||
insertWhereNotExistsCondition?: Condition;
|
insertWhereNotExistsCondition?: Condition;
|
||||||
|
whereNotExistsSource?: Source;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AllowIdentityInsert {
|
export interface AllowIdentityInsert {
|
||||||
@@ -226,6 +227,11 @@ export interface RowNumberExpression {
|
|||||||
orderBy: OrderByExpression[];
|
orderBy: OrderByExpression[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SelectExpression {
|
||||||
|
exprType: 'select';
|
||||||
|
select: Select;
|
||||||
|
}
|
||||||
|
|
||||||
export type Expression =
|
export type Expression =
|
||||||
| ColumnRefExpression
|
| ColumnRefExpression
|
||||||
| ValueExpression
|
| ValueExpression
|
||||||
@@ -235,7 +241,8 @@ export type Expression =
|
|||||||
| CallExpression
|
| CallExpression
|
||||||
| MethodCallExpression
|
| MethodCallExpression
|
||||||
| TranformExpression
|
| TranformExpression
|
||||||
| RowNumberExpression;
|
| RowNumberExpression
|
||||||
|
| SelectExpression;
|
||||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||||
|
|
||||||
export type ResultField = Expression & { alias?: string };
|
export type ResultField = Expression & { alias?: string };
|
||||||
|
|||||||
1
packages/types/dumper.d.ts
vendored
1
packages/types/dumper.d.ts
vendored
@@ -16,6 +16,7 @@ export interface SqlDumper extends AlterProcessor {
|
|||||||
transform(type: TransformType, dumpExpr: () => void);
|
transform(type: TransformType, dumpExpr: () => void);
|
||||||
createDatabase(name: string);
|
createDatabase(name: string);
|
||||||
dropDatabase(name: string);
|
dropDatabase(name: string);
|
||||||
|
comment(value: string);
|
||||||
|
|
||||||
callableTemplate(func: CallableObjectInfo);
|
callableTemplate(func: CallableObjectInfo);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { copyTextToClipboard } from '../utility/clipboard';
|
import { copyTextToClipboard } from '../utility/clipboard';
|
||||||
import { _t, _tval, DefferedTranslationResult } from '../translations';
|
import { _t, _tval, DefferedTranslationResult } from '../translations';
|
||||||
|
import sqlFormatter from 'sql-formatter';
|
||||||
|
|
||||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||||
export const createMatcher =
|
export const createMatcher =
|
||||||
@@ -88,7 +89,8 @@
|
|||||||
isRename?: boolean;
|
isRename?: boolean;
|
||||||
isTruncate?: boolean;
|
isTruncate?: boolean;
|
||||||
isCopyTableName?: boolean;
|
isCopyTableName?: boolean;
|
||||||
isDuplicateTable?: boolean;
|
isTableBackup?: boolean;
|
||||||
|
isTableRestore?: boolean;
|
||||||
isDiagram?: boolean;
|
isDiagram?: boolean;
|
||||||
functionName?: string;
|
functionName?: string;
|
||||||
isExport?: boolean;
|
isExport?: boolean;
|
||||||
@@ -106,6 +108,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
|
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
|
||||||
|
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||||
|
|
||||||
switch (objectTypeField) {
|
switch (objectTypeField) {
|
||||||
case 'tables':
|
case 'tables':
|
||||||
return [
|
return [
|
||||||
@@ -175,9 +179,16 @@
|
|||||||
isCopyTableName: true,
|
isCopyTableName: true,
|
||||||
requiresWriteAccess: false,
|
requiresWriteAccess: false,
|
||||||
},
|
},
|
||||||
hasPermission('dbops/table/backup') && {
|
hasPermission('dbops/table/backup') &&
|
||||||
|
!backupMatch && {
|
||||||
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
|
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
|
||||||
isDuplicateTable: true,
|
isTableBackup: true,
|
||||||
|
requiresWriteAccess: true,
|
||||||
|
},
|
||||||
|
hasPermission('dbops/table/restore') &&
|
||||||
|
backupMatch && {
|
||||||
|
label: _t('dbObject.createRestoreScript', { defaultMessage: 'Create restore script' }),
|
||||||
|
isTableRestore: true,
|
||||||
requiresWriteAccess: true,
|
requiresWriteAccess: true,
|
||||||
},
|
},
|
||||||
hasPermission('dbops/model/view') && {
|
hasPermission('dbops/model/view') && {
|
||||||
@@ -637,7 +648,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (menu.isDuplicateTable) {
|
} else if (menu.isTableBackup) {
|
||||||
const driver = await getDriver();
|
const driver = await getDriver();
|
||||||
const dmp = driver.createDumper();
|
const dmp = driver.createDumper();
|
||||||
const newTable = _.cloneDeep(data);
|
const newTable = _.cloneDeep(data);
|
||||||
@@ -671,6 +682,25 @@
|
|||||||
},
|
},
|
||||||
engine: driver.engine,
|
engine: driver.engine,
|
||||||
});
|
});
|
||||||
|
} else if (menu.isTableRestore) {
|
||||||
|
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||||
|
|
||||||
|
const driver = await getDriver();
|
||||||
|
const dmp = driver.createDumper();
|
||||||
|
const db = await getDatabaseInfo(data);
|
||||||
|
if (db) {
|
||||||
|
const originalTable = db?.tables?.find(x => x.pureName == backupMatch[1] && x.schemaName == data.schemaName);
|
||||||
|
if (originalTable) {
|
||||||
|
createTableRestoreScript(data, originalTable, dmp);
|
||||||
|
newQuery({
|
||||||
|
title: _t('dbObject.restoreScript', {
|
||||||
|
defaultMessage: 'Restore {name} #',
|
||||||
|
values: { name: backupMatch[1] },
|
||||||
|
}),
|
||||||
|
initialData: sqlFormatter.format(dmp.s),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (menu.isImport) {
|
} else if (menu.isImport) {
|
||||||
const { conid, database } = data;
|
const { conid, database } = data;
|
||||||
openImportExportTab({
|
openImportExportTab({
|
||||||
@@ -1008,6 +1038,8 @@
|
|||||||
|
|
||||||
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
|
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TABLE_BACKUP_REGEX = /^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -1025,7 +1057,7 @@
|
|||||||
} from '../stores';
|
} from '../stores';
|
||||||
import openNewTab from '../utility/openNewTab';
|
import openNewTab from '../utility/openNewTab';
|
||||||
import { extractDbNameFromComposite, filterNameCompoud, getConnectionLabel } from 'dbgate-tools';
|
import { extractDbNameFromComposite, filterNameCompoud, getConnectionLabel } from 'dbgate-tools';
|
||||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
||||||
import fullDisplayName from '../utility/fullDisplayName';
|
import fullDisplayName from '../utility/fullDisplayName';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
@@ -1047,6 +1079,8 @@
|
|||||||
import { getBoolSettingsValue, getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
|
import { getBoolSettingsValue, getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
|
||||||
import { isProApp } from '../utility/proTools';
|
import { isProApp } from '../utility/proTools';
|
||||||
import formatFileSize from '../utility/formatFileSize';
|
import formatFileSize from '../utility/formatFileSize';
|
||||||
|
import { createTableRestoreScript } from '../utility/tableRestoreScript';
|
||||||
|
import newQuery from '../query/newQuery';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
export let passProps;
|
export let passProps;
|
||||||
@@ -1087,10 +1121,7 @@
|
|||||||
|
|
||||||
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
||||||
|
|
||||||
$: backupParsed =
|
$: backupParsed = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||||
data.objectTypeField === 'tables'
|
|
||||||
? data.pureName.match(/^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/)
|
|
||||||
: null;
|
|
||||||
$: backupTitle =
|
$: backupTitle =
|
||||||
backupParsed != null
|
backupParsed != null
|
||||||
? `${backupParsed[1]} (${backupParsed[2]}-${backupParsed[3]}-${backupParsed[4]} ${backupParsed[5]}:${backupParsed[6]}:${backupParsed[7]})`
|
? `${backupParsed[1]} (${backupParsed[2]}-${backupParsed[3]}-${backupParsed[4]} ${backupParsed[5]}:${backupParsed[6]}:${backupParsed[7]})`
|
||||||
|
|||||||
132
packages/web/src/utility/tableRestoreScript.ts
Normal file
132
packages/web/src/utility/tableRestoreScript.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { Condition, dumpSqlInsert, dumpSqlUpdate, Insert, Update, Delete, dumpSqlDelete } from 'dbgate-sqltree';
|
||||||
|
import { TableInfo, SqlDumper } from 'dbgate-types';
|
||||||
|
|
||||||
|
export function createTableRestoreScript(backupTable: TableInfo, originalTable: TableInfo, dmp: SqlDumper) {
|
||||||
|
const bothColumns = _.intersection(
|
||||||
|
backupTable.columns.map(x => x.columnName),
|
||||||
|
originalTable.columns.map(x => x.columnName)
|
||||||
|
);
|
||||||
|
const keyColumns = _.intersection(
|
||||||
|
originalTable.primaryKey?.columns?.map(x => x.columnName) || [],
|
||||||
|
backupTable.columns.map(x => x.columnName)
|
||||||
|
);
|
||||||
|
const valueColumns = _.difference(bothColumns, keyColumns);
|
||||||
|
|
||||||
|
function makeColumnCond(colName: string, operator: '=' | '<>' | '<' | '>' | '<=' | '>=' = '='): Condition {
|
||||||
|
return {
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator,
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: colName,
|
||||||
|
source: { name: originalTable },
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: colName,
|
||||||
|
source: { alias: 'bak' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function putTitle(title: string) {
|
||||||
|
dmp.putRaw('\n\n');
|
||||||
|
dmp.comment(`******************** ${title} ********************`);
|
||||||
|
dmp.putRaw('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
dmp.comment(`Restoring data into table ${originalTable.pureName} from backup table ${backupTable.pureName}`);
|
||||||
|
dmp.putRaw('\n');
|
||||||
|
dmp.comment(`Key columns: ${keyColumns.join(', ')}`);
|
||||||
|
dmp.putRaw('\n');
|
||||||
|
dmp.comment(`Value columns: ${valueColumns.join(', ')}`);
|
||||||
|
dmp.putRaw('\n');
|
||||||
|
dmp.comment(`Follows UPDATE, DELETE, INSERT statements to restore data`);
|
||||||
|
dmp.putRaw('\n');
|
||||||
|
|
||||||
|
const update: Update = {
|
||||||
|
commandType: 'update',
|
||||||
|
from: { name: originalTable },
|
||||||
|
fields: valueColumns.map(colName => ({
|
||||||
|
exprType: 'select',
|
||||||
|
select: {
|
||||||
|
commandType: 'select',
|
||||||
|
from: { name: backupTable, alias: 'bak' },
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: colName,
|
||||||
|
source: { alias: 'bak' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: keyColumns.map(colName => makeColumnCond(colName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetColumn: colName,
|
||||||
|
})),
|
||||||
|
where: {
|
||||||
|
conditionType: 'exists',
|
||||||
|
subQuery: {
|
||||||
|
commandType: 'select',
|
||||||
|
from: { name: backupTable, alias: 'bak' },
|
||||||
|
selectAll: true,
|
||||||
|
where: {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: [
|
||||||
|
...keyColumns.map(keyColName => makeColumnCond(keyColName)),
|
||||||
|
{
|
||||||
|
conditionType: 'or',
|
||||||
|
conditions: valueColumns.map(colName => makeColumnCond(colName, '<>')),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
putTitle('UPDATE');
|
||||||
|
dumpSqlUpdate(dmp, update);
|
||||||
|
dmp.endCommand();
|
||||||
|
|
||||||
|
const delcmd: Delete = {
|
||||||
|
commandType: 'delete',
|
||||||
|
from: { name: originalTable },
|
||||||
|
where: {
|
||||||
|
conditionType: 'notExists',
|
||||||
|
subQuery: {
|
||||||
|
commandType: 'select',
|
||||||
|
from: { name: backupTable, alias: 'bak' },
|
||||||
|
selectAll: true,
|
||||||
|
where: {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: keyColumns.map(colName => makeColumnCond(colName)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
putTitle('DELETE');
|
||||||
|
dumpSqlDelete(dmp, delcmd);
|
||||||
|
dmp.endCommand();
|
||||||
|
|
||||||
|
const insert: Insert = {
|
||||||
|
commandType: 'insert',
|
||||||
|
targetTable: originalTable,
|
||||||
|
fields: bothColumns.map(colName => ({
|
||||||
|
targetColumn: colName,
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: colName,
|
||||||
|
source: { alias: 'bak' },
|
||||||
|
})),
|
||||||
|
whereNotExistsSource: { name: backupTable, alias: 'bak' },
|
||||||
|
insertWhereNotExistsCondition: {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: keyColumns.map(colName => makeColumnCond(colName)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
putTitle('INSERT');
|
||||||
|
dumpSqlInsert(dmp, insert);
|
||||||
|
dmp.endCommand();
|
||||||
|
}
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
|
|
||||||
function getAppObjectGroup(data) {
|
function getAppObjectGroup(data) {
|
||||||
if (data.objectTypeField == 'tables') {
|
if (data.objectTypeField == 'tables') {
|
||||||
if (data.pureName.match(/^_(.*)_\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d$/)) {
|
if (data.pureName.match(databaseObjectAppObject.TABLE_BACKUP_REGEX)) {
|
||||||
return _t('dbObject.tableBackups', { defaultMessage: 'Table Backups' });
|
return _t('dbObject.tableBackups', { defaultMessage: 'Table Backups' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user