feat(export-workbench): 支持批量导出工作台并优化 SQL 导出性能

- 侧边栏批量表/批量库入口改为直接打开导出工作台,统一导出配置与进度视图
- 导出工作台新增 batch-tables / batch-databases 模式,支持连接、数据库、对象选择与独立历史记录键
- 连接、数据库、对象下拉项补齐完整名展示与悬浮提示,避免长名称被截断后不可识别
- 后端新增批量对象/批量库导出 WithOptions 链路,统一返回输出文件/目录与进度信息
- SQL dump 数据导出改为按方言批量写入,MySQL/PG 等使用多值 VALUES,Oracle/达梦使用 INSERT ALL
- 补充导出工作台与 SQL dump 的回归测试和 benchmark,覆盖批量模式与批量写入语义
This commit is contained in:
Syngnat
2026-06-17 16:50:05 +08:00
parent 954d126a8f
commit 4e31d47936
10 changed files with 1541 additions and 169 deletions

View File

@@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest';
import {
buildBatchDatabaseExportWorkbenchTab,
buildBatchTableExportWorkbenchTab,
buildExportWorkbenchHistoryKey,
buildTableExportHistoryKey,
buildTableExportTab,
DEFAULT_TABLE_EXPORT_SCOPE_OPTION,
@@ -11,6 +14,20 @@ describe('tableExportTab', () => {
expect(buildTableExportHistoryKey(' conn-1 ', ' app ', ' public.orders ')).toBe('conn-1::app::public.orders');
});
it('builds batch workbench history keys by mode', () => {
expect(buildExportWorkbenchHistoryKey({
connectionId: ' conn-1 ',
dbName: ' app ',
tableName: 'orders',
exportWorkbenchMode: 'batch-tables',
})).toBe('conn-1::app::__batch_tables__');
expect(buildExportWorkbenchHistoryKey({
connectionId: ' conn-1 ',
dbName: ' ignored ',
exportWorkbenchMode: 'batch-databases',
})).toBe('conn-1::__batch_databases__');
});
it('builds a stable table export tab with normalized defaults', () => {
const tab = buildTableExportTab({
connectionId: 'conn-1',
@@ -21,6 +38,7 @@ describe('tableExportTab', () => {
expect(tab.id).toBe('table-export-conn-1-app-public.orders');
expect(tab.type).toBe('table-export');
expect(tab.title).toBe('导出 public.orders');
expect(tab.exportWorkbenchMode).toBe('single');
expect(tab.tableExportScopeOptions).toEqual([DEFAULT_TABLE_EXPORT_SCOPE_OPTION]);
expect(tab.tableExportInitialScope).toBe('all');
expect(tab.tableExportQueryByScope).toBeUndefined();
@@ -60,4 +78,28 @@ describe('tableExportTab', () => {
filteredAll: 42,
});
});
it('builds batch table export workbench tabs with stable ids', () => {
const tab = buildBatchTableExportWorkbenchTab({
connectionId: 'conn-1',
dbName: 'SYS',
});
expect(tab.id).toBe('table-export-batch-tables-conn-1-SYS');
expect(tab.type).toBe('table-export');
expect(tab.title).toBe('批量导出对象');
expect(tab.exportWorkbenchMode).toBe('batch-tables');
expect(tab.dbName).toBe('SYS');
});
it('builds batch database export workbench tabs with stable ids', () => {
const tab = buildBatchDatabaseExportWorkbenchTab({
connectionId: 'conn-1',
});
expect(tab.id).toBe('table-export-batch-databases-conn-1');
expect(tab.type).toBe('table-export');
expect(tab.title).toBe('批量导出库');
expect(tab.exportWorkbenchMode).toBe('batch-databases');
});
});

View File

@@ -18,6 +18,21 @@ export const buildTableExportHistoryKey = (
].join('::');
};
export const buildExportWorkbenchHistoryKey = (
input: Pick<TabData, 'connectionId' | 'dbName' | 'tableName' | 'exportWorkbenchMode'>,
): string => {
const mode = input.exportWorkbenchMode || 'single';
const connectionId = String(input.connectionId || '').trim();
const dbName = String(input.dbName || '').trim();
if (mode === 'batch-tables') {
return [connectionId, dbName, '__batch_tables__'].join('::');
}
if (mode === 'batch-databases') {
return [connectionId, '__batch_databases__'].join('::');
}
return buildTableExportHistoryKey(connectionId, dbName, input.tableName);
};
type BuildTableExportTabInput = {
connectionId: string;
dbName?: string;
@@ -32,6 +47,17 @@ type BuildTableExportTabInput = {
rowCountByScope?: Partial<Record<TableExportScope, number>>;
};
type BuildBatchTableExportWorkbenchTabInput = {
connectionId: string;
dbName?: string;
title?: string;
};
type BuildBatchDatabaseExportWorkbenchTabInput = {
connectionId: string;
title?: string;
};
const normalizeScopeOptions = (
scopeOptions: TableExportScopeOption[] | undefined,
): TableExportScopeOption[] => {
@@ -108,6 +134,7 @@ export const buildTableExportTab = (input: BuildTableExportTabInput): TabData =>
id: `table-export-${connectionId}-${dbName}-${tableName}`,
title: String(input.title || `导出 ${objectLabel}`).trim() || `导出 ${objectLabel}`,
type: 'table-export',
exportWorkbenchMode: 'single',
connectionId,
dbName,
tableName,
@@ -121,3 +148,34 @@ export const buildTableExportTab = (input: BuildTableExportTabInput): TabData =>
tableExportRowCountByScope: normalizeRowCountByScope(input.rowCountByScope),
};
};
export const buildBatchTableExportWorkbenchTab = (
input: BuildBatchTableExportWorkbenchTabInput,
): TabData => {
const connectionId = String(input.connectionId || '').trim();
const dbName = String(input.dbName || '').trim();
const scopeSuffix = dbName || 'all';
return {
id: `table-export-batch-tables-${connectionId || 'none'}-${scopeSuffix}`,
title: String(input.title || '批量导出对象').trim() || '批量导出对象',
type: 'table-export',
exportWorkbenchMode: 'batch-tables',
connectionId,
dbName: dbName || undefined,
initialTab: 'config',
};
};
export const buildBatchDatabaseExportWorkbenchTab = (
input: BuildBatchDatabaseExportWorkbenchTabInput,
): TabData => {
const connectionId = String(input.connectionId || '').trim();
return {
id: `table-export-batch-databases-${connectionId || 'none'}`,
title: String(input.title || '批量导出库').trim() || '批量导出库',
type: 'table-export',
exportWorkbenchMode: 'batch-databases',
connectionId,
initialTab: 'config',
};
};