mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-02 04:29:38 +08:00
🐛 fix(query-export): 修复查询结果导出卡住并统一按数据源能力控制导出路径
- 查询结果页导出增加稳定兜底,异常时确保 loading 关闭避免持续转圈 - DataGrid 导出逻辑按数据源能力分流,优先走后端 ExportQuery 并保留结果集导出降级 - QueryEditor 传递结果导出 SQL,保证查询结果导出范围与当前结果一致 - 后端补充 ExportData/ExportQuery 关键日志,提升导出链路可观测性
This commit is contained in:
@@ -12,6 +12,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { buildOrderBySQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
|
||||
// --- Error Boundary ---
|
||||
interface DataGridErrorBoundaryState {
|
||||
@@ -302,6 +303,7 @@ const DataContext = React.createContext<{
|
||||
copyToClipboard: (t: string) => void;
|
||||
tableName?: string;
|
||||
enableRowContextMenu: boolean;
|
||||
supportsCopyInsert: boolean;
|
||||
} | null>(null);
|
||||
|
||||
interface Item {
|
||||
@@ -444,7 +446,7 @@ const ContextMenuRow = React.memo(({ children, record, ...props }: any) => {
|
||||
|
||||
if (!record || !context) return <tr {...props}>{children}</tr>;
|
||||
|
||||
const { selectedRowKeysRef, displayDataRef, handleCopyInsert, handleCopyJson, handleCopyCsv, handleExportSelected, copyToClipboard, enableRowContextMenu } = context;
|
||||
const { selectedRowKeysRef, displayDataRef, handleCopyInsert, handleCopyJson, handleCopyCsv, handleExportSelected, copyToClipboard, enableRowContextMenu, supportsCopyInsert } = context;
|
||||
|
||||
if (!enableRowContextMenu) {
|
||||
return <tr {...props}>{children}</tr>;
|
||||
@@ -460,12 +462,12 @@ const ContextMenuRow = React.memo(({ children, record, ...props }: any) => {
|
||||
};
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'insert',
|
||||
label: `复制为 INSERT`,
|
||||
icon: <ConsoleSqlOutlined />,
|
||||
onClick: () => handleCopyInsert(record)
|
||||
},
|
||||
...(supportsCopyInsert ? [{
|
||||
key: 'insert',
|
||||
label: '复制为 INSERT',
|
||||
icon: <ConsoleSqlOutlined />,
|
||||
onClick: () => handleCopyInsert(record),
|
||||
}] : []),
|
||||
{ key: 'json', label: '复制为 JSON', icon: <FileTextOutlined />, onClick: () => handleCopyJson(record) },
|
||||
{ key: 'csv', label: '复制为 CSV', icon: <FileTextOutlined />, onClick: () => handleCopyCsv(record) },
|
||||
{ key: 'copy', label: '复制为 Markdown', icon: <CopyOutlined />, onClick: () => {
|
||||
@@ -502,6 +504,8 @@ interface DataGridProps {
|
||||
columnNames: string[];
|
||||
loading: boolean;
|
||||
tableName?: string;
|
||||
exportScope?: 'table' | 'queryResult';
|
||||
resultSql?: string;
|
||||
dbName?: string;
|
||||
connectionId?: string;
|
||||
pkColumns?: string[];
|
||||
@@ -543,7 +547,7 @@ type ColumnMeta = {
|
||||
};
|
||||
|
||||
const DataGrid: React.FC<DataGridProps> = ({
|
||||
data, columnNames, loading, tableName, dbName, connectionId, pkColumns = [], readOnly = false,
|
||||
data, columnNames, loading, tableName, exportScope = 'table', resultSql, dbName, connectionId, pkColumns = [], readOnly = false,
|
||||
onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, onApplyFilter
|
||||
}) => {
|
||||
const connections = useStore(state => state.connections);
|
||||
@@ -559,8 +563,14 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const showColumnComment = queryOptions?.showColumnComment !== false;
|
||||
const showColumnType = queryOptions?.showColumnType !== false;
|
||||
const selectionColumnWidth = 46;
|
||||
const connTypeLower = String(connections.find(c => c.id === connectionId)?.config?.type || '').trim().toLowerCase();
|
||||
const isDuckDBConnection = connTypeLower === 'duckdb';
|
||||
const currentConnConfig = connections.find(c => c.id === connectionId)?.config;
|
||||
const dataSourceCaps = getDataSourceCapabilities(currentConnConfig);
|
||||
const isDuckDBConnection = dataSourceCaps.type === 'duckdb';
|
||||
const supportsCopyInsert = dataSourceCaps.supportsCopyInsert;
|
||||
const supportsSqlQueryExport = dataSourceCaps.supportsSqlQueryExport;
|
||||
const isQueryResultExport = exportScope === 'queryResult';
|
||||
const canImport = exportScope === 'table' && !!tableName;
|
||||
const canExport = !!connectionId && (isQueryResultExport || !!tableName);
|
||||
|
||||
// Background Helper
|
||||
const getBg = (darkHex: string) => {
|
||||
@@ -687,11 +697,20 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// Helper to export specific data
|
||||
const exportData = async (rows: any[], format: string) => {
|
||||
const hide = message.loading(`正在导出 ${rows.length} 条数据...`, 0);
|
||||
const cleanRows = rows.map(({ [GONAVI_ROW_KEY]: _rowKey, ...rest }) => rest);
|
||||
// Pass tableName (or 'export') as default filename
|
||||
const res = await ExportData(cleanRows, columnNames, tableName || 'export', format);
|
||||
hide();
|
||||
if (res.success) { message.success("导出成功"); } else if (res.message !== "Cancelled") { message.error("导出失败: " + res.message); }
|
||||
try {
|
||||
const cleanRows = rows.map(({ [GONAVI_ROW_KEY]: _rowKey, ...rest }) => rest);
|
||||
// Pass tableName (or 'export') as default filename
|
||||
const res = await ExportData(cleanRows, columnNames, tableName || 'export', format);
|
||||
if (res.success) {
|
||||
message.success("导出成功");
|
||||
} else if (res.message !== "Cancelled") {
|
||||
message.error("导出失败: " + res.message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error("导出失败: " + (e?.message || String(e)));
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(null);
|
||||
@@ -2101,6 +2120,10 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}, []);
|
||||
|
||||
const handleCopyInsert = useCallback((record: any) => {
|
||||
if (!supportsCopyInsert) {
|
||||
message.warning("当前数据源不支持复制为 INSERT,请使用 JSON/CSV/Markdown 复制。");
|
||||
return;
|
||||
}
|
||||
const records = getTargets(record);
|
||||
const sqls = records.map((r: any) => {
|
||||
const { [GONAVI_ROW_KEY]: _rowKey, ...vals } = r;
|
||||
@@ -2110,7 +2133,7 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return `INSERT INTO \`${targetTable}\` (${cols.map(c => `\`${c}\``).join(', ')}) VALUES (${values.join(', ')});`;
|
||||
});
|
||||
copyToClipboard(sqls.join('\n'));
|
||||
}, [tableName, getTargets, copyToClipboard]);
|
||||
}, [supportsCopyInsert, tableName, getTargets, copyToClipboard]);
|
||||
|
||||
const handleCopyJson = useCallback((record: any) => {
|
||||
const records = getTargets(record);
|
||||
@@ -2149,12 +2172,17 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const config = buildConnConfig();
|
||||
if (!config) return;
|
||||
const hide = message.loading(`正在导出...`, 0);
|
||||
const res = await ExportQuery(config as any, dbName || '', sql, defaultName || 'export', format);
|
||||
hide();
|
||||
if (res.success) {
|
||||
message.success("导出成功");
|
||||
} else if (res.message !== "Cancelled") {
|
||||
message.error("导出失败: " + res.message);
|
||||
try {
|
||||
const res = await ExportQuery(config as any, dbName || '', sql, defaultName || 'export', format);
|
||||
if (res.success) {
|
||||
message.success("导出成功");
|
||||
} else if (res.message !== "Cancelled") {
|
||||
message.error("导出失败: " + res.message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error("导出失败: " + (e?.message || String(e)));
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
}, [buildConnConfig, dbName]);
|
||||
|
||||
@@ -2198,6 +2226,10 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
// Context Menu Export
|
||||
const handleExportSelected = useCallback(async (format: string, record: any) => {
|
||||
const records = getTargets(record);
|
||||
if (isQueryResultExport) {
|
||||
await exportData(records, format);
|
||||
return;
|
||||
}
|
||||
if (!connectionId || !tableName) {
|
||||
await exportData(records, format);
|
||||
return;
|
||||
@@ -2225,11 +2257,11 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
|
||||
const sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} WHERE ${pkWhere}`;
|
||||
await exportByQuery(sql, format, tableName || 'export');
|
||||
}, [getTargets, connectionId, tableName, hasChanges, exportData, buildConnConfig, buildPkWhereSql, exportByQuery]);
|
||||
}, [getTargets, isQueryResultExport, connectionId, tableName, hasChanges, exportData, buildConnConfig, buildPkWhereSql, exportByQuery]);
|
||||
|
||||
// Export
|
||||
const handleExport = async (format: string) => {
|
||||
if (!connectionId || !tableName) return;
|
||||
if (!connectionId) return;
|
||||
|
||||
// 1. Export Selected
|
||||
if (selectedRowKeys.length > 0) {
|
||||
@@ -2238,17 +2270,38 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询结果页导出统一按当前结果集(已加载数据)导出,避免再次执行原 SQL 造成大数据导出或长时间阻塞。
|
||||
if (isQueryResultExport) {
|
||||
const sql = String(resultSql || '').trim();
|
||||
if (!hasChanges && supportsSqlQueryExport && sql) {
|
||||
await exportByQuery(sql, format, tableName || 'query_result');
|
||||
} else {
|
||||
await exportData(mergedDisplayData, format);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Prompt for Current vs All
|
||||
// Using a custom modal content with buttons to handle 3 states
|
||||
let instance: any;
|
||||
const handleAll = async () => {
|
||||
instance.destroy();
|
||||
if (!tableName) return;
|
||||
const config = buildConnConfig();
|
||||
if (!config) return;
|
||||
const hide = message.loading(`正在导出全部数据...`, 0);
|
||||
const res = await ExportTable(config as any, dbName || '', tableName, format);
|
||||
hide();
|
||||
if (res.success) { message.success("导出成功"); } else if (res.message !== "Cancelled") { message.error("导出失败: " + res.message); }
|
||||
try {
|
||||
const res = await ExportTable(config as any, dbName || '', tableName, format);
|
||||
if (res.success) {
|
||||
message.success("导出成功");
|
||||
} else if (res.message !== "Cancelled") {
|
||||
message.error("导出失败: " + res.message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error("导出失败: " + (e?.message || String(e)));
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
const handlePage = async () => {
|
||||
instance.destroy();
|
||||
@@ -2411,7 +2464,8 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
copyToClipboard,
|
||||
tableName,
|
||||
enableRowContextMenu: !canModifyData,
|
||||
}), [handleCopyCsv, handleCopyInsert, handleCopyJson, handleExportSelected, copyToClipboard, tableName, canModifyData]);
|
||||
supportsCopyInsert,
|
||||
}), [handleCopyCsv, handleCopyInsert, handleCopyJson, handleExportSelected, copyToClipboard, tableName, canModifyData, supportsCopyInsert]);
|
||||
|
||||
const cellContextMenuValue = useMemo(() => ({
|
||||
showMenu: showCellContextMenu,
|
||||
@@ -2456,8 +2510,8 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
setSelectedRowKeys([]);
|
||||
onReload();
|
||||
}}>刷新</Button>}
|
||||
{tableName && <Button icon={<ImportOutlined />} onClick={handleImport}>导入</Button>}
|
||||
{tableName && <Dropdown menu={{ items: exportMenu }}><Button icon={<ExportOutlined />}>导出 <DownOutlined /></Button></Dropdown>}
|
||||
{canImport && <Button icon={<ImportOutlined />} onClick={handleImport}>导入</Button>}
|
||||
{canExport && <Dropdown menu={{ items: exportMenu }}><Button icon={<ExportOutlined />}>导出 <DownOutlined /></Button></Dropdown>}
|
||||
|
||||
{canModifyData && (
|
||||
<>
|
||||
@@ -2996,21 +3050,23 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
填充到选中行 ({selectedRowKeys.length})
|
||||
</div>
|
||||
<div style={{ height: 1, background: darkMode ? '#303030' : '#f0f0f0', margin: '4px 0' }} />
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = darkMode ? '#303030' : '#f5f5f5'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||
onClick={() => {
|
||||
if (cellContextMenu.record) handleCopyInsert(cellContextMenu.record);
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
}}
|
||||
>
|
||||
复制为 INSERT
|
||||
</div>
|
||||
{supportsCopyInsert && (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = darkMode ? '#303030' : '#f5f5f5'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
||||
onClick={() => {
|
||||
if (cellContextMenu.record) handleCopyInsert(cellContextMenu.record);
|
||||
setCellContextMenu(prev => ({ ...prev, visible: false }));
|
||||
}}
|
||||
>
|
||||
复制为 INSERT
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useStore } from '../store';
|
||||
import { DBQuery, DBGetColumns } from '../../wailsjs/go/app/App';
|
||||
import DataGrid, { GONAVI_ROW_KEY } from './DataGrid';
|
||||
import { buildOrderBySQL, buildWhereSQL, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
|
||||
type ViewerPaginationState = {
|
||||
current: number;
|
||||
@@ -172,8 +173,10 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const [filterConditions, setFilterConditions] = useState<FilterCondition[]>([]);
|
||||
const duckdbSafeSelectCacheRef = useRef<Record<string, string>>({});
|
||||
const currentConnType = (connections.find(c => c.id === tab.connectionId)?.config?.type || '').toLowerCase();
|
||||
const forceReadOnly = currentConnType === 'tdengine' || currentConnType === 'clickhouse';
|
||||
const currentConnConfig = connections.find(c => c.id === tab.connectionId)?.config;
|
||||
const currentConnCaps = getDataSourceCapabilities(currentConnConfig);
|
||||
const currentConnType = currentConnCaps.type;
|
||||
const forceReadOnly = currentConnCaps.forceReadOnlyQueryResult;
|
||||
|
||||
useEffect(() => {
|
||||
setPkColumns([]);
|
||||
@@ -673,6 +676,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
columnNames={columnNames}
|
||||
loading={loading}
|
||||
tableName={tab.tableName}
|
||||
exportScope="table"
|
||||
dbName={tab.dbName}
|
||||
connectionId={tab.connectionId}
|
||||
pkColumns={pkColumns}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import Editor, { OnMount } from '@monaco-editor/react';
|
||||
import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select, Tabs } from 'antd';
|
||||
import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
@@ -7,6 +7,7 @@ import { TabData, ColumnDefinition } from '../types';
|
||||
import { useStore } from '../store';
|
||||
import { DBQuery, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns } from '../../wailsjs/go/app/App';
|
||||
import DataGrid, { GONAVI_ROW_KEY } from './DataGrid';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
|
||||
const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const [query, setQuery] = useState(tab.query || 'SELECT * FROM ');
|
||||
@@ -14,6 +15,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
type ResultSet = {
|
||||
key: string;
|
||||
sql: string;
|
||||
exportSql?: string;
|
||||
rows: any[];
|
||||
columns: string[];
|
||||
tableName?: string;
|
||||
@@ -47,6 +49,10 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const visibleDbsRef = useRef<string[]>([]); // Store visible databases for cross-db intellisense
|
||||
|
||||
const connections = useStore(state => state.connections);
|
||||
const queryCapableConnections = useMemo(
|
||||
() => connections.filter(c => getDataSourceCapabilities(c.config).supportsQueryEditor),
|
||||
[connections]
|
||||
);
|
||||
const addSqlLog = useStore(state => state.addSqlLog);
|
||||
const currentConnectionIdRef = useRef(currentConnectionId);
|
||||
const currentDbRef = useRef(currentDb);
|
||||
@@ -64,6 +70,16 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
currentConnectionIdRef.current = currentConnectionId;
|
||||
}, [currentConnectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryCapableConnections.some(c => c.id === currentConnectionId)) {
|
||||
const fallback = queryCapableConnections[0]?.id || '';
|
||||
if (fallback && fallback !== currentConnectionId) {
|
||||
setCurrentConnectionId(fallback);
|
||||
setCurrentDb('');
|
||||
}
|
||||
}
|
||||
}, [queryCapableConnections, currentConnectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
currentDbRef.current = currentDb;
|
||||
}, [currentDb]);
|
||||
@@ -977,6 +993,12 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
if (runSeqRef.current === runSeq) setLoading(false);
|
||||
return;
|
||||
}
|
||||
const connCaps = getDataSourceCapabilities(conn.config);
|
||||
if (!connCaps.supportsQueryEditor) {
|
||||
message.error("当前数据源不支持 SQL 查询编辑器,请使用对应专用页面。");
|
||||
if (runSeqRef.current === runSeq) setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
...conn.config,
|
||||
@@ -1000,8 +1022,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const nextResultSets: ResultSet[] = [];
|
||||
const maxRows = Number(queryOptions?.maxRows) || 0;
|
||||
const dbType = String((config as any).type || 'mysql');
|
||||
const normalizedDbType = dbType.toLowerCase();
|
||||
const forceReadOnlyResult = normalizedDbType === 'tdengine' || normalizedDbType === 'clickhouse';
|
||||
const forceReadOnlyResult = connCaps.forceReadOnlyQueryResult;
|
||||
const wantsLimitProbe = Number.isFinite(maxRows) && maxRows > 0;
|
||||
const probeLimit = wantsLimitProbe ? (maxRows + 1) : 0;
|
||||
let anyTruncated = false;
|
||||
@@ -1066,6 +1087,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
nextResultSets.push({
|
||||
key: `result-${idx + 1}`,
|
||||
sql: rawStatement,
|
||||
exportSql: limited.applied ? applyAutoLimit(rawStatement, dbType, Math.max(1, Number(maxRows) || 1)).sql : rawStatement,
|
||||
rows,
|
||||
columns: cols,
|
||||
tableName: simpleTableName,
|
||||
@@ -1082,6 +1104,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
nextResultSets.push({
|
||||
key: `result-${idx + 1}`,
|
||||
sql: rawStatement,
|
||||
exportSql: rawStatement,
|
||||
rows: [row],
|
||||
columns: ['affectedRows'],
|
||||
pkColumns: [],
|
||||
@@ -1223,7 +1246,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
setCurrentConnectionId(val);
|
||||
setCurrentDb('');
|
||||
}}
|
||||
options={connections.map(c => ({ label: c.name, value: c.id }))}
|
||||
options={queryCapableConnections.map(c => ({ label: c.name, value: c.id }))}
|
||||
showSearch
|
||||
/>
|
||||
<Select
|
||||
@@ -1333,6 +1356,8 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
columnNames={rs.columns}
|
||||
loading={loading}
|
||||
tableName={rs.tableName}
|
||||
exportScope="queryResult"
|
||||
resultSql={rs.exportSql || rs.sql}
|
||||
dbName={currentDb}
|
||||
connectionId={currentConnectionId}
|
||||
pkColumns={rs.pkColumns}
|
||||
|
||||
86
frontend/src/utils/dataSourceCapabilities.ts
Normal file
86
frontend/src/utils/dataSourceCapabilities.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ConnectionConfig } from '../types';
|
||||
|
||||
type ConnectionLike = Pick<ConnectionConfig, 'type' | 'driver'> | null | undefined;
|
||||
|
||||
const normalizeDataSourceToken = (raw: string): string => {
|
||||
const normalized = String(raw || '').trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'doris':
|
||||
return 'diros';
|
||||
case 'postgresql':
|
||||
return 'postgres';
|
||||
case 'dm':
|
||||
return 'dameng';
|
||||
default:
|
||||
return normalized;
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveDataSourceType = (config: ConnectionLike): string => {
|
||||
if (!config) return '';
|
||||
const type = normalizeDataSourceToken(String(config.type || ''));
|
||||
if (type === 'custom') {
|
||||
const driver = normalizeDataSourceToken(String(config.driver || ''));
|
||||
return driver || 'custom';
|
||||
}
|
||||
return type;
|
||||
};
|
||||
|
||||
const SQL_QUERY_EXPORT_TYPES = new Set([
|
||||
'mysql',
|
||||
'mariadb',
|
||||
'diros',
|
||||
'sphinx',
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'sqlserver',
|
||||
'sqlite',
|
||||
'duckdb',
|
||||
'oracle',
|
||||
'dameng',
|
||||
'tdengine',
|
||||
'clickhouse',
|
||||
]);
|
||||
|
||||
const COPY_INSERT_TYPES = new Set([
|
||||
'mysql',
|
||||
'mariadb',
|
||||
'diros',
|
||||
'sphinx',
|
||||
'postgres',
|
||||
'kingbase',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'sqlserver',
|
||||
'sqlite',
|
||||
'duckdb',
|
||||
'oracle',
|
||||
'dameng',
|
||||
'tdengine',
|
||||
'clickhouse',
|
||||
]);
|
||||
|
||||
const QUERY_EDITOR_DISABLED_TYPES = new Set(['redis']);
|
||||
const FORCE_READ_ONLY_QUERY_TYPES = new Set(['tdengine', 'clickhouse']);
|
||||
|
||||
export type DataSourceCapabilities = {
|
||||
type: string;
|
||||
supportsQueryEditor: boolean;
|
||||
supportsSqlQueryExport: boolean;
|
||||
supportsCopyInsert: boolean;
|
||||
forceReadOnlyQueryResult: boolean;
|
||||
};
|
||||
|
||||
export const getDataSourceCapabilities = (config: ConnectionLike): DataSourceCapabilities => {
|
||||
const type = resolveDataSourceType(config);
|
||||
return {
|
||||
type,
|
||||
supportsQueryEditor: !QUERY_EDITOR_DISABLED_TYPES.has(type),
|
||||
supportsSqlQueryExport: SQL_QUERY_EXPORT_TYPES.has(type),
|
||||
supportsCopyInsert: COPY_INSERT_TYPES.has(type),
|
||||
forceReadOnlyQueryResult: FORCE_READ_ONLY_QUERY_TYPES.has(type),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user