feat(data-transfer): 完善数据库 SQL 备份与恢复

- 数据导入工作台新增数据库模式,支持 SQL 文件选择、目标库、进度、取消与重试
- 新增受连接保护约束的 ImportDatabaseSQL 接口并复用流式执行链路
- SQL 导出支持切换数据库上下文,兼顾原库备份和跨库导入
- 调整侧栏与表概览入口,备份先进入工作台确认配置再执行
- 同步 Wails 绑定、Web 运行时限制、六语文案和回归测试
This commit is contained in:
Syngnat
2026-07-23 16:40:44 +08:00
parent 1f17b5cfad
commit d618ef381e
38 changed files with 2183 additions and 182 deletions

View File

@@ -12,6 +12,7 @@ describe('dataImportTab', () => {
connectionId: ' conn-1 ',
dbName: ' app ',
tableName: ' public.users ',
launchKey: ' launch-table-1 ',
title: ' 数据导入 ',
});
@@ -22,6 +23,8 @@ describe('dataImportTab', () => {
connectionId: 'conn-1',
dbName: 'app',
tableName: 'public.users',
dataImportMode: 'table',
dataImportLaunchKey: 'launch-table-1',
initialTab: 'target',
});
});
@@ -33,6 +36,26 @@ describe('dataImportTab', () => {
expect(tab.connectionId).toBe('');
expect(tab.dbName).toBeUndefined();
expect(tab.tableName).toBeUndefined();
expect(tab.dataImportMode).toBe('table');
expect(tab.dataImportLaunchKey).toMatch(/^data-import-/);
});
it('builds a database import launch that clears a stale table target', () => {
const tab = buildDataImportWorkbenchTab({
connectionId: 'conn-1',
dbName: 'app',
tableName: 'users',
mode: 'database',
launchKey: 'database-launch-1',
});
expect(tab).toMatchObject({
connectionId: 'conn-1',
dbName: 'app',
dataImportMode: 'database',
dataImportLaunchKey: 'database-launch-1',
});
expect(tab).toHaveProperty('tableName', undefined);
});
it('keeps the active target when the stable workbench is reopened during an import', () => {
@@ -41,6 +64,7 @@ describe('dataImportTab', () => {
connectionId: 'conn-1',
dbName: 'app',
tableName: 'users',
launchKey: 'running-launch',
}),
dataImportRunning: true,
};
@@ -49,6 +73,8 @@ describe('dataImportTab', () => {
connectionId: 'conn-2',
dbName: 'analytics',
tableName: 'events',
mode: 'database',
launchKey: 'ignored-launch',
})).toBe(existing);
});
});

View File

@@ -3,10 +3,14 @@ import type { TabData } from '../types';
export const DATA_IMPORT_WORKBENCH_TAB_ID = 'data-import-workbench';
export type DataImportMode = 'table' | 'database';
export type BuildDataImportWorkbenchTabInput = {
connectionId?: string;
dbName?: string;
tableName?: string;
mode?: DataImportMode;
launchKey?: string;
title?: string;
};
@@ -17,16 +21,23 @@ const normalizeOptionalText = (value: string | undefined): string | undefined =>
export const buildDataImportWorkbenchTab = (
input: BuildDataImportWorkbenchTabInput = {},
): TabData => ({
id: DATA_IMPORT_WORKBENCH_TAB_ID,
title: String(input.title || t('data_import.workbench.title')).trim()
|| t('data_import.workbench.title'),
type: 'data-import' as TabData['type'],
connectionId: normalizeOptionalText(input.connectionId) || '',
dbName: normalizeOptionalText(input.dbName),
tableName: normalizeOptionalText(input.tableName),
initialTab: 'target',
});
): TabData => {
const mode: DataImportMode = input.mode === 'database' ? 'database' : 'table';
const launchKey = normalizeOptionalText(input.launchKey)
|| `data-import-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return {
id: DATA_IMPORT_WORKBENCH_TAB_ID,
title: String(input.title || t('data_import.workbench.title')).trim()
|| t('data_import.workbench.title'),
type: 'data-import' as TabData['type'],
connectionId: normalizeOptionalText(input.connectionId) || '',
dbName: normalizeOptionalText(input.dbName),
tableName: mode === 'table' ? normalizeOptionalText(input.tableName) : undefined,
dataImportMode: mode,
dataImportLaunchKey: launchKey,
initialTab: 'target',
};
};
export const resolveDataImportWorkbenchLaunchTab = (
existingTab: TabData | undefined,

View File

@@ -126,10 +126,43 @@ describe('tableExportTab', () => {
tableExportInitialObjectNames: ['users', 'orders'],
tableExportContentMode: 'backup',
tableExportIncludeDropIfExists: true,
tableExportLaunchKey: 'batch-tables-1',
tableExportRequestKey: 'batch-tables-1',
}));
});
it('carries launch-only refresh keys without creating auto-start requests', () => {
const tableTab = buildBatchTableExportWorkbenchTab({
connectionId: 'conn-1',
dbName: 'SYS',
initialObjectNames: [' users '],
contentMode: 'backup',
includeDropIfExists: false,
launchKey: ' table-launch-1 ',
});
const databaseTab = buildDatabaseExportWorkbenchTab({
connectionId: 'conn-1',
dbName: ' app ',
contentMode: 'schema',
launchKey: ' database-launch-1 ',
});
expect(tableTab).toEqual(expect.objectContaining({
tableExportInitialObjectNames: ['users'],
tableExportContentMode: 'backup',
tableExportIncludeDropIfExists: false,
tableExportLaunchKey: 'table-launch-1',
}));
expect(databaseTab).toEqual(expect.objectContaining({
exportWorkbenchMode: 'database',
dbName: 'app',
tableExportContentMode: 'schema',
tableExportLaunchKey: 'database-launch-1',
}));
expect(tableTab).toHaveProperty('tableExportRequestKey', undefined);
expect(databaseTab).toHaveProperty('tableExportRequestKey', undefined);
});
it('builds batch database export workbench tabs with stable ids', () => {
setCurrentLanguage('zh-CN');
const tab = buildBatchDatabaseExportWorkbenchTab({
@@ -155,6 +188,7 @@ describe('tableExportTab', () => {
tableExportInitialDatabaseNames: ['app', 'audit'],
tableExportContentMode: 'schema',
tableExportIncludeDropIfExists: true,
tableExportLaunchKey: 'batch-databases-1',
tableExportRequestKey: 'batch-databases-1',
}));
});
@@ -180,6 +214,7 @@ describe('tableExportTab', () => {
exportWorkbenchMode: 'database',
dbName: 'app',
tableExportContentMode: 'backup',
tableExportLaunchKey: 'database-1',
tableExportRequestKey: 'database-1',
}));
expect(schemaTab).toEqual(expect.objectContaining({
@@ -189,6 +224,7 @@ describe('tableExportTab', () => {
dbName: 'app',
schemaName: 'sales',
tableExportContentMode: 'schema',
tableExportLaunchKey: 'schema-1',
tableExportRequestKey: 'schema-1',
}));
});

View File

@@ -47,6 +47,7 @@ export const buildExportWorkbenchHistoryKey = (
type ExportWorkbenchLaunchOptions = {
contentMode?: TableExportContentMode;
includeDropIfExists?: boolean;
launchKey?: string;
requestKey?: string;
};
@@ -103,11 +104,18 @@ const normalizeNameList = (values: string[] | undefined): string[] | undefined =
return result.length > 0 ? result : undefined;
};
const buildLaunchMetadata = (input: ExportWorkbenchLaunchOptions): Partial<TabData> => ({
...(input.contentMode ? { tableExportContentMode: input.contentMode } : {}),
...(input.includeDropIfExists === true ? { tableExportIncludeDropIfExists: true } : {}),
...(String(input.requestKey || '').trim() ? { tableExportRequestKey: String(input.requestKey).trim() } : {}),
});
const buildLaunchMetadata = (input: ExportWorkbenchLaunchOptions): Partial<TabData> => {
const requestKey = String(input.requestKey || '').trim();
const launchKey = String(input.launchKey || '').trim() || requestKey;
return {
...(input.contentMode ? { tableExportContentMode: input.contentMode } : {}),
...(input.includeDropIfExists !== undefined
? { tableExportIncludeDropIfExists: input.includeDropIfExists === true }
: {}),
tableExportLaunchKey: launchKey || undefined,
tableExportRequestKey: requestKey || undefined,
};
};
const normalizeScopeOptions = (
scopeOptions: TableExportScopeOption[] | undefined,