feat(sidebar-redis-db): 新增库表重命名删除、批量仅数据导出与Redis多选删键能力

- 后端新增数据库/表重命名与删除能力,覆盖多数据源差异处理
- 批量操作表新增“仅导出数据(INSERT)”模式并完善导出链路
- Redis Key 列表支持分组展示、勾选批量删除与当前Key删除入口
- 同步 Wails 前后端绑定接口并优化批量操作弹窗按钮布局
This commit is contained in:
Syngnat
2026-02-06 16:57:05 +08:00
parent 70cb6148c6
commit 820b064e7f
6 changed files with 796 additions and 221 deletions

View File

@@ -1,13 +1,16 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Table, Input, Button, Space, Tag, message, Modal, Form, InputNumber, Popconfirm, Tooltip, Radio } from 'antd';
import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined } from '@ant-design/icons';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Table, Input, Button, Space, Tag, Tree, Spin, message, Modal, Form, InputNumber, Popconfirm, Tooltip, Radio } from 'antd';
import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined, FolderOpenOutlined, KeyOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { RedisKeyInfo, RedisValue } from '../types';
import Editor from '@monaco-editor/react';
import type { ColumnType } from 'antd/es/table';
import type { DataNode } from 'antd/es/tree';
const { Search } = Input;
const KEY_GROUP_DELIMITER = ':';
const EMPTY_SEGMENT_LABEL = '(empty)';
interface RedisViewerProps {
connectionId: string;
redisDB: number;
@@ -222,86 +225,167 @@ const ResizableDivider: React.FC<{
};
// 可拖拽列头组件 - 纯 DOM 操作实现
const ResizableTitle: React.FC<any> = (props) => {
const { onResize, width, children, ...restProps } = props;
const thRef = useRef<HTMLTableCellElement>(null);
type RedisKeyTreeLeaf = {
keyInfo: RedisKeyInfo;
label: string;
};
// 如果没有 onResize 或 width说明这列不需要拖拽如复选框列
if (!onResize || !width) {
return <th {...restProps}>{children}</th>;
}
type RedisKeyTreeGroup = {
name: string;
path: string;
children: Map<string, RedisKeyTreeGroup>;
leaves: RedisKeyTreeLeaf[];
};
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
type RedisKeyTreeResult = {
treeData: DataNode[];
rawKeyByNodeKey: Map<string, string>;
leafNodeKeyByRawKey: Map<string, string>;
groupKeys: string[];
};
const startX = e.clientX;
const startWidth = width;
const th = thRef.current;
if (!th) return;
const normalizeKeySegment = (segment: string): string => {
return segment === '' ? EMPTY_SEGMENT_LABEL : segment;
};
// 找到对应的 colgroup col 元素来同步更新列宽
const table = th.closest('table');
const thIndex = Array.from(th.parentElement?.children || []).indexOf(th);
const col = table?.querySelector(`colgroup col:nth-child(${thIndex + 1})`) as HTMLElement | null;
const createTreeGroup = (name: string, path: string): RedisKeyTreeGroup => {
return { name, path, children: new Map(), leaves: [] };
};
// 创建遮罩层防止文本选择
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;cursor:col-resize;z-index:9999;';
document.body.appendChild(overlay);
const countGroupLeafNodes = (group: RedisKeyTreeGroup): number => {
let count = group.leaves.length;
group.children.forEach((child) => {
count += countGroupLeafNodes(child);
});
return count;
};
let currentWidth = startWidth;
const buildRedisKeyTree = (
keys: RedisKeyInfo[],
formatTTL: (ttl: number) => string,
getTypeColor: (type: string) => string
): RedisKeyTreeResult => {
const root = createTreeGroup('__root__', '__root__');
const handleMouseMove = (moveEvent: MouseEvent) => {
moveEvent.preventDefault();
const delta = moveEvent.clientX - startX;
currentWidth = Math.max(50, startWidth + delta);
// 直接操作 DOM
th.style.width = `${currentWidth}px`;
if (col) {
col.style.width = `${currentWidth}px`;
keys.forEach((keyInfo) => {
const segments = keyInfo.key.split(KEY_GROUP_DELIMITER);
if (segments.length <= 1) {
root.leaves.push({ keyInfo, label: keyInfo.key });
return;
}
const groupSegments = segments.slice(0, -1);
const leafLabel = normalizeKeySegment(segments[segments.length - 1]);
let current = root;
const pathParts: string[] = [];
groupSegments.forEach((segment) => {
const normalized = normalizeKeySegment(segment);
pathParts.push(normalized);
const groupPath = pathParts.join(KEY_GROUP_DELIMITER);
let child = current.children.get(normalized);
if (!child) {
child = createTreeGroup(normalized, groupPath);
current.children.set(normalized, child);
}
};
current = child;
});
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.removeChild(overlay);
// 拖拽结束时更新 React state
onResize(null, { size: { width: currentWidth } });
};
current.leaves.push({ keyInfo, label: leafLabel });
});
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
const rawKeyByNodeKey = new Map<string, string>();
const leafNodeKeyByRawKey = new Map<string, string>();
const groupKeys: string[] = [];
const toTreeNodes = (group: RedisKeyTreeGroup): DataNode[] => {
const childGroups = Array.from(group.children.values()).sort((a, b) => a.name.localeCompare(b.name));
const childLeaves = [...group.leaves].sort((a, b) => a.keyInfo.key.localeCompare(b.keyInfo.key));
const groupNodes: DataNode[] = childGroups.map((child) => {
const groupNodeKey = `group:${child.path}`;
groupKeys.push(groupNodeKey);
return {
key: groupNodeKey,
title: (
<Space size={6}>
<FolderOpenOutlined style={{ color: '#8c8c8c' }} />
<span>{child.name}</span>
<span style={{ fontSize: 12, color: '#999' }}>({countGroupLeafNodes(child)})</span>
</Space>
),
selectable: false,
disableCheckbox: true,
children: toTreeNodes(child),
};
});
const leafNodes: DataNode[] = childLeaves.map((leaf) => {
const nodeKey = `key:${leaf.keyInfo.key}`;
rawKeyByNodeKey.set(nodeKey, leaf.keyInfo.key);
leafNodeKeyByRawKey.set(leaf.keyInfo.key, nodeKey);
return {
key: nodeKey,
isLeaf: true,
title: (
<div
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) 92px 92px',
columnGap: 8,
alignItems: 'center',
minWidth: 0,
width: '100%',
}}
>
<Space size={6} style={{ minWidth: 0 }}>
<KeyOutlined style={{ color: '#1677ff' }} />
<Tooltip title={leaf.keyInfo.key}>
<span
style={{
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
verticalAlign: 'bottom',
}}
>
{leaf.label}
</span>
</Tooltip>
</Space>
<Tag
color={getTypeColor(leaf.keyInfo.type)}
style={{ marginInlineEnd: 0, width: '100%', textAlign: 'center' }}
>
{leaf.keyInfo.type}
</Tag>
<span
style={{
width: '100%',
fontSize: 12,
color: '#999',
textAlign: 'left',
whiteSpace: 'nowrap',
}}
>
{formatTTL(leaf.keyInfo.ttl)}
</span>
</div>
),
};
});
return [...groupNodes, ...leafNodes];
};
return (
<th
ref={thRef}
{...restProps}
style={{
...restProps.style,
position: 'relative'
}}
>
{children}
<div
style={{
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: 10,
cursor: 'col-resize',
zIndex: 1,
background: 'transparent'
}}
onMouseDown={handleMouseDown}
onMouseOver={(e) => { e.currentTarget.style.background = 'rgba(0,0,0,0.06)'; }}
onMouseOut={(e) => { e.currentTarget.style.background = 'transparent'; }}
/>
</th>
);
return {
treeData: toTreeNodes(root),
rawKeyByNodeKey,
leafNodeKeyByRawKey,
groupKeys,
};
};
const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
@@ -317,7 +401,6 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const [keyValue, setKeyValue] = useState<RedisValue | null>(null);
const [valueLoading, setValueLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editForm] = Form.useForm();
const [newKeyModalOpen, setNewKeyModalOpen] = useState(false);
const [newKeyForm] = Form.useForm();
const [ttlModalOpen, setTtlModalOpen] = useState(false);
@@ -341,15 +424,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
// 面板宽度状态和 ref - 默认占据 50% 宽度
const [leftPanelWidth, setLeftPanelWidth] = useState<number | string>('50%');
const leftPanelRef = useRef<HTMLDivElement>(null);
// 列宽状态 - 复选框列约 32px总宽度需要接近面板宽度
// Key 列自适应剩余空间,其他列固定宽度
const [columnWidths, setColumnWidths] = useState({
key: 220, // Key 名称,需要较宽
type: 65, // 类型标签
ttl: 80, // TTL 显示
action: 50 // 操作按钮
});
const [expandedGroupKeys, setExpandedGroupKeys] = useState<string[]>([]);
const getConfig = useCallback(() => {
if (!connection) return null;
@@ -373,7 +448,12 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
if (res.success) {
const result = res.data;
if (append) {
setKeys(prev => [...prev, ...result.keys]);
setKeys(prev => {
const keyMap = new Map<string, RedisKeyInfo>();
prev.forEach(item => keyMap.set(item.key, item));
result.keys.forEach((item: RedisKeyInfo) => keyMap.set(item.key, item));
return Array.from(keyMap.values());
});
} else {
setKeys(result.keys);
}
@@ -451,6 +531,11 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
}
};
const handleDeleteCurrentKey = async () => {
if (!selectedKey) return;
await handleDeleteKeys([selectedKey]);
};
const handleSetTTL = async () => {
const config = getConfig();
if (!config || !selectedKey) return;
@@ -529,65 +614,54 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return `${Math.floor(ttl / 86400)}${Math.floor((ttl % 86400) / 3600)}`;
};
// 处理列宽调整 - react-resizable 的 onResize 回调格式
const handleColumnResize = (key: string) => (_e: any, { size }: { size: { width: number } }) => {
setColumnWidths(prev => ({ ...prev, [key]: size.width }));
const keyTree = useMemo(() => {
return buildRedisKeyTree(keys, formatTTL, getTypeColor);
}, [keys]);
const selectedTreeNodeKeys = useMemo(() => {
if (!selectedKey) {
return [] as string[];
}
const nodeKey = keyTree.leafNodeKeyByRawKey.get(selectedKey);
return nodeKey ? [nodeKey] : [];
}, [selectedKey, keyTree]);
const checkedTreeNodeKeys = useMemo(() => {
return selectedKeys
.map(rawKey => keyTree.leafNodeKeyByRawKey.get(rawKey))
.filter((nodeKey): nodeKey is string => Boolean(nodeKey));
}, [selectedKeys, keyTree]);
useEffect(() => {
const existingKeySet = new Set(keys.map(item => item.key));
setSelectedKeys(prev => prev.filter(rawKey => existingKeySet.has(rawKey)));
}, [keys]);
useEffect(() => {
setExpandedGroupKeys((prev) => {
const validKeys = prev.filter(nodeKey => keyTree.groupKeys.includes(nodeKey));
return validKeys;
});
}, [keyTree]);
const handleTreeSelect = (nodeKeys: React.Key[]) => {
if (nodeKeys.length === 0) {
return;
}
const rawKey = keyTree.rawKeyByNodeKey.get(String(nodeKeys[0]));
if (!rawKey) {
return;
}
loadKeyValue(rawKey);
};
const columns: ColumnType<RedisKeyInfo>[] = [
{
title: 'Key',
dataIndex: 'key',
key: 'key',
width: columnWidths.key,
ellipsis: true,
onHeaderCell: (column: any) => ({
width: column.width,
onResize: handleColumnResize('key')
}),
render: (text: string) => (
<Tooltip title={text}>
<span style={{ cursor: 'pointer' }} onClick={() => loadKeyValue(text)}>{text}</span>
</Tooltip>
)
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: columnWidths.type,
onHeaderCell: (column: any) => ({
width: column.width,
onResize: handleColumnResize('type')
}),
render: (type: string) => <Tag color={getTypeColor(type)}>{type}</Tag>
},
{
title: 'TTL',
dataIndex: 'ttl',
key: 'ttl',
width: columnWidths.ttl,
onHeaderCell: (column: any) => ({
width: column.width,
onResize: handleColumnResize('ttl')
}),
render: (ttl: number) => formatTTL(ttl)
},
{
title: '操作',
key: 'action',
width: columnWidths.action,
onHeaderCell: (column: any) => ({
width: column.width,
onResize: handleColumnResize('action')
}),
render: (_: any, record: RedisKeyInfo) => (
<Popconfirm title="确定删除此 Key" onConfirm={() => handleDeleteKeys([record.key])}>
<Button type="text" danger size="small" icon={<DeleteOutlined />} />
</Popconfirm>
)
}
];
const handleTreeCheck = (checked: React.Key[] | { checked: React.Key[]; halfChecked: React.Key[] }) => {
const checkedNodeKeys = Array.isArray(checked) ? checked : checked.checked;
const rawKeys = checkedNodeKeys
.map(nodeKey => keyTree.rawKeyByNodeKey.get(String(nodeKey)))
.filter((rawKey): rawKey is string => Boolean(rawKey));
setSelectedKeys(rawKeys);
};
const renderValueEditor = () => {
if (!keyValue || !selectedKey) {
@@ -1375,6 +1449,9 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
setTtlModalOpen(true);
}}> TTL</Button>
<Button size="small" onClick={() => loadKeyValue(selectedKey)} icon={<ReloadOutlined />}></Button>
<Popconfirm title={`确定删除 Key "${selectedKey}"`} onConfirm={handleDeleteCurrentKey}>
<Button size="small" danger icon={<DeleteOutlined />}> Key</Button>
</Popconfirm>
</Space>
</div>
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
@@ -1410,36 +1487,35 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
<Button size="small" icon={<ReloadOutlined />} onClick={handleRefresh}></Button>
<Button size="small" icon={<PlusOutlined />} onClick={() => setNewKeyModalOpen(true)}></Button>
</Space>
{selectedKeys.length > 0 && (
<Popconfirm title={`确定删除选中的 ${selectedKeys.length} 个 Key`} onConfirm={() => handleDeleteKeys(selectedKeys)}>
<Button size="small" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
)}
<Popconfirm
title={`确定删除选中的 ${selectedKeys.length} 个 Key`}
onConfirm={() => handleDeleteKeys(selectedKeys)}
disabled={selectedKeys.length === 0}
>
<Button size="small" danger icon={<DeleteOutlined />} disabled={selectedKeys.length === 0}>
({selectedKeys.length})
</Button>
</Popconfirm>
</div>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
<Table
dataSource={keys}
columns={columns}
rowKey="key"
size="small"
loading={loading}
pagination={false}
components={{
header: {
cell: ResizableTitle
}
}}
rowSelection={{
selectedRowKeys: selectedKeys,
onChange: (keys) => setSelectedKeys(keys as string[])
}}
onRow={(record) => ({
onClick: () => loadKeyValue(record.key),
style: { cursor: 'pointer', background: selectedKey === record.key ? '#e6f7ff' : undefined }
})}
style={{ width: '100%' }}
/>
<Spin spinning={loading} size="small">
<Tree
blockNode
showIcon={false}
checkable
checkStrictly
selectable
treeData={keyTree.treeData}
selectedKeys={selectedTreeNodeKeys}
checkedKeys={checkedTreeNodeKeys}
expandedKeys={expandedGroupKeys}
onExpand={(nextExpandedKeys) => setExpandedGroupKeys(nextExpandedKeys as string[])}
onSelect={(nodeKeys) => handleTreeSelect(nodeKeys)}
onCheck={(checked) => handleTreeCheck(checked)}
style={{ padding: '8px 6px' }}
/>
</Spin>
{hasMore && (
<div style={{ padding: 8, textAlign: 'center' }}>
<Button onClick={handleLoadMore} loading={loading}></Button>

View File

@@ -28,7 +28,7 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge,
} from '@ant-design/icons';
import { useStore } from '../store';
import { SavedConnection } from '../types';
import { DBGetDatabases, DBGetTables, DBShowCreateTable, ExportTable, OpenSQLFile, CreateDatabase } from '../../wailsjs/go/app/App';
import { DBGetDatabases, DBGetTables, DBShowCreateTable, ExportTable, OpenSQLFile, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable } from '../../wailsjs/go/app/App';
import { normalizeOpacityForPlatform } from '../utils/appearance';
const { Search } = Input;
@@ -43,6 +43,8 @@ interface TreeNode {
type?: 'connection' | 'database' | 'table' | 'queries-folder' | 'saved-query' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db';
}
type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly';
const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> = ({ onEditConnection }) => {
const connections = useStore(state => state.connections);
const savedQueries = useStore(state => state.savedQueries);
@@ -96,6 +98,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false);
const [createDbForm] = Form.useForm();
const [targetConnection, setTargetConnection] = useState<any>(null);
const [isRenameDbModalOpen, setIsRenameDbModalOpen] = useState(false);
const [renameDbForm] = Form.useForm();
const [renameDbTarget, setRenameDbTarget] = useState<any>(null);
const [isRenameTableModalOpen, setIsRenameTableModalOpen] = useState(false);
const [renameTableForm] = Form.useForm();
const [renameTableTarget, setRenameTableTarget] = useState<any>(null);
// Batch Operations Modal
const [isBatchModalOpen, setIsBatchModalOpen] = useState(false);
@@ -661,7 +669,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
}
};
const handleBatchExport = async (includeData: boolean) => {
const handleBatchExport = async (mode: BatchTableExportMode) => {
const selectedTables = batchTables.filter(t => checkedTableKeys.includes(t.key));
if (selectedTables.length === 0) {
message.warning('请至少选择一张表');
@@ -673,9 +681,17 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const { conn, dbName } = batchDbContext;
const tableNames = selectedTables.map(t => t.tableName);
const hide = message.loading(includeData ? `正在备份选中表 (${tableNames.length})...` : `正在导出选中表结构 (${tableNames.length})...`, 0);
const loadingText = mode === 'backup'
? `正在备份选中表 (${tableNames.length})...`
: mode === 'dataOnly'
? `正在导出选中表数据 (INSERT) (${tableNames.length})...`
: `正在导出选中表结构 (${tableNames.length})...`;
const hide = message.loading(loadingText, 0);
try {
const res = await (window as any).go.app.App.ExportTablesSQL(normalizeConnConfig(conn.config), dbName, tableNames, includeData);
const app = (window as any).go.app.App;
const res = mode === 'dataOnly'
? await app.ExportTablesDataSQL(normalizeConnConfig(conn.config), dbName, tableNames)
: await app.ExportTablesSQL(normalizeConnConfig(conn.config), dbName, tableNames, mode === 'backup');
hide();
if (res.success) {
message.success('导出成功');
@@ -865,6 +881,148 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
}
};
const buildRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => {
return {
...conn.config,
port: Number(conn.config.port),
password: conn.config.password || "",
database: clearDatabase ? "" : ((overrideDatabase ?? conn.config.database) || ""),
useSSH: conn.config.useSSH || false,
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
};
};
const getConnectionNodeRef = (connRef: any) => {
const latestConn = connections.find(c => c.id === connRef.id);
return { key: connRef.id, dataRef: latestConn || connRef };
};
const getDatabaseNodeRef = (connRef: any, dbName: string) => {
const latestConn = connections.find(c => c.id === connRef.id);
return {
key: `${connRef.id}-${dbName}`,
dataRef: { ...(latestConn || connRef), dbName }
};
};
const extractObjectName = (fullName: string) => {
const raw = String(fullName || '').trim();
const idx = raw.lastIndexOf('.');
if (idx >= 0 && idx < raw.length - 1) {
return raw.substring(idx + 1);
}
return raw;
};
const handleRenameDatabase = async () => {
if (!renameDbTarget) return;
try {
const values = await renameDbForm.validateFields();
const conn = renameDbTarget.dataRef;
const oldDbName = String(conn.dbName || '').trim();
const newDbName = String(values.newName || '').trim();
if (!oldDbName || !newDbName) {
message.error("数据库名称不能为空");
return;
}
if (oldDbName === newDbName) {
message.warning("新旧数据库名称相同,无需修改");
return;
}
const config = buildRuntimeConfig(conn, conn.dbName);
const res = await RenameDatabase(config as any, oldDbName, newDbName);
if (res.success) {
message.success("数据库重命名成功");
setExpandedKeys(prev => prev.filter(k => !k.toString().startsWith(`${conn.id}-${oldDbName}`)));
setLoadedKeys(prev => prev.filter(k => !k.toString().startsWith(`${conn.id}-${oldDbName}`)));
await loadDatabases(getConnectionNodeRef(conn));
setIsRenameDbModalOpen(false);
setRenameDbTarget(null);
renameDbForm.resetFields();
} else {
message.error("重命名失败: " + res.message);
}
} catch (e) {
// Validate failed
}
};
const handleDeleteDatabase = (node: any) => {
const conn = node.dataRef;
const dbName = String(conn.dbName || '').trim();
if (!dbName) return;
Modal.confirm({
title: '确认删除数据库',
content: `确定删除数据库 "${dbName}" 吗?该操作不可恢复。`,
okButtonProps: { danger: true },
onOk: async () => {
const config = buildRuntimeConfig(conn, conn.dbName);
const res = await DropDatabase(config as any, dbName);
if (res.success) {
message.success("数据库删除成功");
setExpandedKeys(prev => prev.filter(k => !k.toString().startsWith(`${conn.id}-${dbName}`)));
setLoadedKeys(prev => prev.filter(k => !k.toString().startsWith(`${conn.id}-${dbName}`)));
await loadDatabases(getConnectionNodeRef(conn));
} else {
message.error("删除失败: " + res.message);
}
}
});
};
const handleRenameTable = async () => {
if (!renameTableTarget) return;
try {
const values = await renameTableForm.validateFields();
const conn = renameTableTarget.dataRef;
const oldTableName = String(conn.tableName || '').trim();
const newTableName = String(values.newName || '').trim();
if (!oldTableName || !newTableName) {
message.error("表名不能为空");
return;
}
if (extractObjectName(oldTableName) === newTableName || oldTableName === newTableName) {
message.warning("新旧表名相同,无需修改");
return;
}
const config = buildRuntimeConfig(conn, conn.dbName);
const res = await RenameTable(config as any, conn.dbName, oldTableName, newTableName);
if (res.success) {
message.success("表重命名成功");
await loadTables(getDatabaseNodeRef(conn, conn.dbName));
setIsRenameTableModalOpen(false);
setRenameTableTarget(null);
renameTableForm.resetFields();
} else {
message.error("重命名失败: " + res.message);
}
} catch (e) {
// Validate failed
}
};
const handleDeleteTable = (node: any) => {
const conn = node.dataRef;
const tableName = String(conn.tableName || '').trim();
if (!tableName) return;
Modal.confirm({
title: '确认删除表',
content: `确定删除表 "${tableName}" 吗?该操作不可恢复。`,
okButtonProps: { danger: true },
onOk: async () => {
const config = buildRuntimeConfig(conn, conn.dbName);
const res = await DropTable(config as any, conn.dbName, tableName);
if (res.success) {
message.success("表删除成功");
await loadTables(getDatabaseNodeRef(conn, conn.dbName));
} else {
message.error("删除失败: " + res.message);
}
}
});
};
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setSearchValue(value);
@@ -1088,6 +1246,23 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
icon: <TableOutlined />,
onClick: () => openNewTableDesign(node)
},
{
key: 'rename-db',
label: '重命名数据库',
icon: <EditOutlined />,
onClick: () => {
setRenameDbTarget(node);
renameDbForm.setFieldsValue({ newName: node.dataRef?.dbName || '' });
setIsRenameDbModalOpen(true);
}
},
{
key: 'drop-db',
label: '删除数据库',
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDeleteDatabase(node)
},
{
key: 'refresh',
label: '刷新',
@@ -1180,6 +1355,23 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
icon: <SaveOutlined />,
onClick: () => handleExport(node, 'sql')
},
{
key: 'rename-table',
label: '重命名表',
icon: <EditOutlined />,
onClick: () => {
setRenameTableTarget(node);
renameTableForm.setFieldsValue({ newName: extractObjectName(node.dataRef?.tableName || node.title) });
setIsRenameTableModalOpen(true);
}
},
{
key: 'drop-table',
label: '删除表',
icon: <DeleteOutlined />,
danger: true,
onClick: () => handleDeleteTable(node)
},
{
type: 'divider'
},
@@ -1295,33 +1487,79 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
</Form>
</Modal>
<Modal
title={`重命名数据库${renameDbTarget?.dataRef?.dbName ? ` (${renameDbTarget.dataRef.dbName})` : ''}`}
open={isRenameDbModalOpen}
onOk={handleRenameDatabase}
onCancel={() => {
setIsRenameDbModalOpen(false);
setRenameDbTarget(null);
renameDbForm.resetFields();
}}
>
<Form form={renameDbForm} layout="vertical">
<Form.Item name="newName" label="新数据库名称" rules={[{ required: true, message: '请输入新数据库名称' }]}>
<Input />
</Form.Item>
</Form>
</Modal>
<Modal
title={`重命名表${renameTableTarget?.dataRef?.tableName ? ` (${renameTableTarget.dataRef.tableName})` : ''}`}
open={isRenameTableModalOpen}
onOk={handleRenameTable}
onCancel={() => {
setIsRenameTableModalOpen(false);
setRenameTableTarget(null);
renameTableForm.resetFields();
}}
>
<Form form={renameTableForm} layout="vertical">
<Form.Item name="newName" label="新表名" rules={[{ required: true, message: '请输入新表名' }]}>
<Input />
</Form.Item>
</Form>
</Modal>
<Modal
title="批量操作表"
open={isBatchModalOpen}
onCancel={() => setIsBatchModalOpen(false)}
width={600}
footer={[
<Button key="cancel" onClick={() => setIsBatchModalOpen(false)}>
</Button>,
<Button
key="export-schema"
icon={<ExportOutlined />}
onClick={() => handleBatchExport(false)}
disabled={checkedTableKeys.length === 0}
>
({checkedTableKeys.length})
</Button>,
<Button
key="backup"
type="primary"
icon={<SaveOutlined />}
onClick={() => handleBatchExport(true)}
disabled={checkedTableKeys.length === 0}
>
({checkedTableKeys.length})
</Button>
]}
width={680}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<Button key="cancel" onClick={() => setIsBatchModalOpen(false)}>
</Button>
<Space size={8} wrap style={{ marginLeft: 'auto' }}>
<Button
key="export-schema"
icon={<ExportOutlined />}
onClick={() => handleBatchExport('schema')}
disabled={checkedTableKeys.length === 0}
>
</Button>
<Button
key="export-data-only"
icon={<SaveOutlined />}
onClick={() => handleBatchExport('dataOnly')}
disabled={checkedTableKeys.length === 0}
>
(INSERT)
</Button>
<Button
key="backup"
type="primary"
icon={<SaveOutlined />}
onClick={() => handleBatchExport('backup')}
disabled={checkedTableKeys.length === 0}
>
(+)
</Button>
</Space>
</div>
}
>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8 }}>

View File

@@ -38,6 +38,10 @@ export function DataSyncPreview(arg1:sync.SyncConfig,arg2:string,arg3:number):Pr
export function DownloadUpdate():Promise<connection.QueryResult>;
export function DropDatabase(arg1:connection.ConnectionConfig,arg2:string):Promise<connection.QueryResult>;
export function DropTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise<connection.QueryResult>;
export function ExportData(arg1:Array<Record<string, any>>,arg2:Array<string>,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function ExportDatabaseSQL(arg1:connection.ConnectionConfig,arg2:string,arg3:boolean):Promise<connection.QueryResult>;
@@ -46,6 +50,8 @@ export function ExportQuery(arg1:connection.ConnectionConfig,arg2:string,arg3:st
export function ExportTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function ExportTablesDataSQL(arg1:connection.ConnectionConfig,arg2:string,arg3:Array<string>):Promise<connection.QueryResult>;
export function ExportTablesSQL(arg1:connection.ConnectionConfig,arg2:string,arg3:Array<string>,arg4:boolean):Promise<connection.QueryResult>;
export function GetAppInfo():Promise<connection.QueryResult>;
@@ -110,4 +116,8 @@ export function RedisZSetAdd(arg1:connection.ConnectionConfig,arg2:string,arg3:A
export function RedisZSetRemove(arg1:connection.ConnectionConfig,arg2:string,arg3:Array<string>):Promise<connection.QueryResult>;
export function RenameDatabase(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise<connection.QueryResult>;
export function RenameTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function TestConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;

View File

@@ -70,6 +70,14 @@ export function DownloadUpdate() {
return window['go']['app']['App']['DownloadUpdate']();
}
export function DropDatabase(arg1, arg2) {
return window['go']['app']['App']['DropDatabase'](arg1, arg2);
}
export function DropTable(arg1, arg2, arg3) {
return window['go']['app']['App']['DropTable'](arg1, arg2, arg3);
}
export function ExportData(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['ExportData'](arg1, arg2, arg3, arg4);
}
@@ -86,6 +94,10 @@ export function ExportTable(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['ExportTable'](arg1, arg2, arg3, arg4);
}
export function ExportTablesDataSQL(arg1, arg2, arg3) {
return window['go']['app']['App']['ExportTablesDataSQL'](arg1, arg2, arg3);
}
export function ExportTablesSQL(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['ExportTablesSQL'](arg1, arg2, arg3, arg4);
}
@@ -214,6 +226,14 @@ export function RedisZSetRemove(arg1, arg2, arg3) {
return window['go']['app']['App']['RedisZSetRemove'](arg1, arg2, arg3);
}
export function RenameDatabase(arg1, arg2, arg3) {
return window['go']['app']['App']['RenameDatabase'](arg1, arg2, arg3);
}
export function RenameTable(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['RenameTable'](arg1, arg2, arg3, arg4);
}
export function TestConnection(arg1) {
return window['go']['app']['App']['TestConnection'](arg1);
}

View File

@@ -20,7 +20,7 @@ func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResu
logger.Error(err, "DBConnect 连接失败:%s", formatConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
logger.Infof("DBConnect 连接成功:%s", formatConnSummary(config))
return connection.QueryResult{Success: true, Message: "连接成功"}
}
@@ -31,14 +31,14 @@ func (a *App) TestConnection(config connection.ConnectionConfig) connection.Quer
logger.Error(err, "TestConnection 连接测试失败:%s", formatConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
logger.Infof("TestConnection 连接测试成功:%s", formatConnSummary(config))
return connection.QueryResult{Success: true, Message: "连接成功"}
}
func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string) connection.QueryResult {
runConfig := config
runConfig.Database = ""
runConfig.Database = ""
dbInst, err := a.getDatabase(runConfig)
if err != nil {
@@ -60,6 +60,221 @@ func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string)
return connection.QueryResult{Success: true, Message: "Database created successfully"}
}
func resolveDDLDBType(config connection.ConnectionConfig) string {
dbType := strings.ToLower(strings.TrimSpace(config.Type))
if dbType != "custom" {
return dbType
}
driver := strings.ToLower(strings.TrimSpace(config.Driver))
switch driver {
case "postgresql":
return "postgres"
case "dm":
return "dameng"
case "sqlite3":
return "sqlite"
default:
return driver
}
}
func normalizeSchemaAndTableByType(dbType string, dbName string, tableName string) (string, string) {
rawTable := strings.TrimSpace(tableName)
rawDB := strings.TrimSpace(dbName)
if rawTable == "" {
return rawDB, rawTable
}
if parts := strings.SplitN(rawTable, ".", 2); len(parts) == 2 {
schema := strings.TrimSpace(parts[0])
table := strings.TrimSpace(parts[1])
if schema != "" && table != "" {
return schema, table
}
}
switch dbType {
case "postgres", "kingbase":
return "public", rawTable
default:
return rawDB, rawTable
}
}
func quoteTableIdentByType(dbType string, schema string, table string) string {
s := strings.TrimSpace(schema)
t := strings.TrimSpace(table)
if s == "" {
return quoteIdentByType(dbType, t)
}
return fmt.Sprintf("%s.%s", quoteIdentByType(dbType, s), quoteIdentByType(dbType, t))
}
func buildRunConfigForDDL(config connection.ConnectionConfig, dbType string, dbName string) connection.ConnectionConfig {
runConfig := normalizeRunConfig(config, dbName)
if strings.EqualFold(strings.TrimSpace(config.Type), "custom") {
// custom 连接的 dbName 语义依赖 driver尽量在常见驱动上对齐内置类型行为。
switch dbType {
case "mysql", "postgres", "kingbase", "dameng":
if strings.TrimSpace(dbName) != "" {
runConfig.Database = strings.TrimSpace(dbName)
}
}
}
return runConfig
}
func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string, newName string) connection.QueryResult {
oldName = strings.TrimSpace(oldName)
newName = strings.TrimSpace(newName)
if oldName == "" || newName == "" {
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
}
if strings.EqualFold(oldName, newName) {
return connection.QueryResult{Success: false, Message: "新旧数据库名称不能相同"}
}
dbType := resolveDDLDBType(config)
switch dbType {
case "mysql":
return connection.QueryResult{Success: false, Message: "MySQL 不支持直接重命名数据库,请新建库后迁移数据"}
case "postgres", "kingbase":
if strings.EqualFold(strings.TrimSpace(config.Database), oldName) {
return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"}
}
runConfig := config
if strings.TrimSpace(runConfig.Database) == "" {
runConfig.Database = "postgres"
}
dbInst, err := a.getDatabase(runConfig)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
sql := fmt.Sprintf("ALTER DATABASE %s RENAME TO %s", quoteIdentByType(dbType, oldName), quoteIdentByType(dbType, newName))
if _, err := dbInst.Exec(sql); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: "数据库重命名成功"}
default:
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名数据库", dbType)}
}
}
func (a *App) DropDatabase(config connection.ConnectionConfig, dbName string) connection.QueryResult {
dbName = strings.TrimSpace(dbName)
if dbName == "" {
return connection.QueryResult{Success: false, Message: "数据库名称不能为空"}
}
dbType := resolveDDLDBType(config)
var (
runConfig connection.ConnectionConfig
sql string
)
switch dbType {
case "mysql":
runConfig = config
runConfig.Database = ""
sql = fmt.Sprintf("DROP DATABASE %s", quoteIdentByType(dbType, dbName))
case "postgres", "kingbase":
if strings.EqualFold(strings.TrimSpace(config.Database), dbName) {
return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再删除"}
}
runConfig = config
if strings.TrimSpace(runConfig.Database) == "" {
runConfig.Database = "postgres"
}
sql = fmt.Sprintf("DROP DATABASE %s", quoteIdentByType(dbType, dbName))
default:
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除数据库", dbType)}
}
dbInst, err := a.getDatabase(runConfig)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if _, err := dbInst.Exec(sql); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: "数据库删除成功"}
}
func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, oldTableName string, newTableName string) connection.QueryResult {
oldTableName = strings.TrimSpace(oldTableName)
newTableName = strings.TrimSpace(newTableName)
if oldTableName == "" || newTableName == "" {
return connection.QueryResult{Success: false, Message: "表名不能为空"}
}
if strings.EqualFold(oldTableName, newTableName) {
return connection.QueryResult{Success: false, Message: "新旧表名不能相同"}
}
if strings.Contains(newTableName, ".") {
return connection.QueryResult{Success: false, Message: "新表名不能包含 schema 或数据库前缀"}
}
dbType := resolveDDLDBType(config)
switch dbType {
case "mysql", "postgres", "kingbase", "sqlite", "oracle", "dameng":
default:
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名表", dbType)}
}
schemaName, pureOldTableName := normalizeSchemaAndTableByType(dbType, dbName, oldTableName)
if pureOldTableName == "" {
return connection.QueryResult{Success: false, Message: "旧表名不能为空"}
}
oldQualifiedTable := quoteTableIdentByType(dbType, schemaName, pureOldTableName)
newTableQuoted := quoteIdentByType(dbType, newTableName)
sql := fmt.Sprintf("ALTER TABLE %s RENAME TO %s", oldQualifiedTable, newTableQuoted)
if dbType == "mysql" {
newQualifiedTable := quoteTableIdentByType(dbType, schemaName, newTableName)
sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualifiedTable, newQualifiedTable)
}
runConfig := buildRunConfigForDDL(config, dbType, dbName)
dbInst, err := a.getDatabase(runConfig)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if _, err := dbInst.Exec(sql); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: "表重命名成功"}
}
func (a *App) DropTable(config connection.ConnectionConfig, dbName string, tableName string) connection.QueryResult {
tableName = strings.TrimSpace(tableName)
if tableName == "" {
return connection.QueryResult{Success: false, Message: "表名不能为空"}
}
dbType := resolveDDLDBType(config)
switch dbType {
case "mysql", "postgres", "kingbase", "sqlite", "oracle", "dameng":
default:
return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除表", dbType)}
}
schemaName, pureTableName := normalizeSchemaAndTableByType(dbType, dbName, tableName)
if pureTableName == "" {
return connection.QueryResult{Success: false, Message: "表名不能为空"}
}
qualifiedTable := quoteTableIdentByType(dbType, schemaName, pureTableName)
sql := fmt.Sprintf("DROP TABLE %s", qualifiedTable)
runConfig := buildRunConfigForDDL(config, dbType, dbName)
dbInst, err := a.getDatabase(runConfig)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if _, err := dbInst.Exec(sql); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
return connection.QueryResult{Success: true, Message: "表删除成功"}
}
func (a *App) MySQLConnect(config connection.ConnectionConfig) connection.QueryResult {
config.Type = "mysql"
return a.DBConnect(config)
@@ -156,12 +371,12 @@ func (a *App) DBGetDatabases(config connection.ConnectionConfig) connection.Quer
logger.Error(err, "DBGetDatabases 获取数据库列表失败:%s", formatConnSummary(config))
return connection.QueryResult{Success: false, Message: err.Error()}
}
var resData []map[string]string
for _, name := range dbs {
resData = append(resData, map[string]string{"Database": name})
}
return connection.QueryResult{Success: true, Data: resData}
}

View File

@@ -102,8 +102,8 @@ func (a *App) ImportData(config connection.ConnectionConfig, dbName, tableName s
}
defer f.Close()
var rows []map[string]interface{ }
var rows []map[string]interface{}
if strings.HasSuffix(strings.ToLower(selection), ".json") {
decoder := json.NewDecoder(f)
if err := decoder.Decode(&rows); err != nil {
@@ -120,7 +120,7 @@ func (a *App) ImportData(config connection.ConnectionConfig, dbName, tableName s
}
headers := records[0]
for _, record := range records[1:] {
row := make(map[string]interface{ })
row := make(map[string]interface{})
for i, val := range record {
if i < len(headers) {
if val == "NULL" {
@@ -153,7 +153,7 @@ func (a *App) ImportData(config connection.ConnectionConfig, dbName, tableName s
for k := range firstRow {
cols = append(cols, k)
}
for _, row := range rows {
var values []string
for _, col := range cols {
@@ -195,7 +195,7 @@ func (a *App) ApplyChanges(config connection.ConnectionConfig, dbName, tableName
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if applier, ok := dbInst.(db.BatchApplier); ok {
err := applier.ApplyChanges(tableName, changes)
if err != nil {
@@ -219,7 +219,7 @@ func (a *App) ExportTable(config connection.ConnectionConfig, dbName string, tab
runConfig := normalizeRunConfig(config, dbName)
dbInst, err := a.getDatabase(runConfig)
dbInst, err := a.getDatabase(runConfig)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
@@ -238,7 +238,7 @@ dbInst, err := a.getDatabase(runConfig)
if err := writeSQLHeader(w, runConfig, dbName); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := dumpTableSQL(w, dbInst, runConfig, dbName, tableName, true); err != nil {
if err := dumpTableSQL(w, dbInst, runConfig, dbName, tableName, true, true); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if err := writeSQLFooter(w, runConfig); err != nil {
@@ -249,8 +249,8 @@ dbInst, err := a.getDatabase(runConfig)
}
query := fmt.Sprintf("SELECT * FROM %s", quoteQualifiedIdentByType(runConfig.Type, tableName))
data, columns, err := dbInst.Query(query)
data, columns, err := dbInst.Query(query)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
@@ -268,13 +268,27 @@ data, columns, err := dbInst.Query(query)
}
func (a *App) ExportTablesSQL(config connection.ConnectionConfig, dbName string, tableNames []string, includeData bool) connection.QueryResult {
return a.exportTablesSQL(config, dbName, tableNames, true, includeData)
}
func (a *App) ExportTablesDataSQL(config connection.ConnectionConfig, dbName string, tableNames []string) connection.QueryResult {
return a.exportTablesSQL(config, dbName, tableNames, false, true)
}
func (a *App) exportTablesSQL(config connection.ConnectionConfig, dbName string, tableNames []string, includeSchema bool, includeData bool) connection.QueryResult {
if !includeSchema && !includeData {
return connection.QueryResult{Success: false, Message: "invalid export mode"}
}
safeDbName := strings.TrimSpace(dbName)
if safeDbName == "" {
safeDbName = "export"
}
suffix := "schema"
if includeData {
if includeSchema && includeData {
suffix = "backup"
} else if !includeSchema && includeData {
suffix = "data"
}
defaultFilename := fmt.Sprintf("%s_%s_%dtables.sql", safeDbName, suffix, len(tableNames))
if len(tableNames) == 1 && strings.TrimSpace(tableNames[0]) != "" {
@@ -323,7 +337,7 @@ func (a *App) ExportTablesSQL(config connection.ConnectionConfig, dbName string,
return connection.QueryResult{Success: false, Message: err.Error()}
}
for _, t := range tables {
if err := dumpTableSQL(w, dbInst, runConfig, dbName, t, includeData); err != nil {
if err := dumpTableSQL(w, dbInst, runConfig, dbName, t, includeSchema, includeData); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
}
@@ -377,7 +391,7 @@ func (a *App) ExportDatabaseSQL(config connection.ConnectionConfig, dbName strin
return connection.QueryResult{Success: false, Message: err.Error()}
}
for _, t := range tables {
if err := dumpTableSQL(w, dbInst, runConfig, dbName, t, includeData); err != nil {
if err := dumpTableSQL(w, dbInst, runConfig, dbName, t, true, includeData); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
}
@@ -534,7 +548,7 @@ func formatSQLValue(dbType string, v interface{}) string {
}
}
func dumpTableSQL(w *bufio.Writer, dbInst db.Database, config connection.ConnectionConfig, dbName, tableName string, includeData bool) error {
func dumpTableSQL(w *bufio.Writer, dbInst db.Database, config connection.ConnectionConfig, dbName, tableName string, includeSchema bool, includeData bool) error {
schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
if _, err := w.WriteString("\n-- ----------------------------\n"); err != nil {
@@ -547,15 +561,17 @@ func dumpTableSQL(w *bufio.Writer, dbInst db.Database, config connection.Connect
return err
}
createSQL, err := dbInst.GetCreateStatement(schemaName, pureTableName)
if err != nil {
return err
}
if _, err := w.WriteString(ensureSQLTerminator(createSQL)); err != nil {
return err
}
if _, err := w.WriteString("\n\n"); err != nil {
return err
if includeSchema {
createSQL, err := dbInst.GetCreateStatement(schemaName, pureTableName)
if err != nil {
return err
}
if _, err := w.WriteString(ensureSQLTerminator(createSQL)); err != nil {
return err
}
if _, err := w.WriteString("\n\n"); err != nil {
return err
}
}
if !includeData {