import React, { useEffect, useState, useCallback, useRef } from 'react'; import { message } from 'antd'; import { TabData, ColumnDefinition } from '../types'; import { useStore } from '../store'; import { DBQuery, DBGetColumns } from '../../wailsjs/go/app/App'; import DataGrid, { GONAVI_ROW_KEY } from './DataGrid'; import { buildOrderBySQL, buildWhereSQL, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql'; const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const [data, setData] = useState([]); const [columnNames, setColumnNames] = useState([]); const [pkColumns, setPkColumns] = useState([]); const [loading, setLoading] = useState(false); const connections = useStore(state => state.connections); const addSqlLog = useStore(state => state.addSqlLog); const fetchSeqRef = useRef(0); const countSeqRef = useRef(0); const countKeyRef = useRef(''); const pkSeqRef = useRef(0); const pkKeyRef = useRef(''); const [pagination, setPagination] = useState({ current: 1, pageSize: 100, total: 0, totalKnown: false }); const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(null); const [showFilter, setShowFilter] = useState(false); const [filterConditions, setFilterConditions] = useState([]); const currentConnType = (connections.find(c => c.id === tab.connectionId)?.config?.type || '').toLowerCase(); const forceReadOnly = currentConnType === 'tdengine'; useEffect(() => { setPkColumns([]); pkKeyRef.current = ''; countKeyRef.current = ''; setPagination(prev => ({ ...prev, current: 1, total: 0, totalKnown: false })); }, [tab.connectionId, tab.dbName, tab.tableName]); const fetchData = useCallback(async (page = pagination.current, size = pagination.pageSize) => { const seq = ++fetchSeqRef.current; setLoading(true); const conn = connections.find(c => c.id === tab.connectionId); if (!conn) { message.error("Connection not found"); if (fetchSeqRef.current === seq) setLoading(false); return; } const config = { ...conn.config, port: Number(conn.config.port), password: conn.config.password || "", database: conn.config.database || "", useSSH: conn.config.useSSH || false, ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } }; const dbType = config.type || ''; const dbTypeLower = String(dbType || '').trim().toLowerCase(); const isMySQLFamily = dbTypeLower === 'mysql' || dbTypeLower === 'mariadb'; const dbName = tab.dbName || ''; const tableName = tab.tableName || ''; const whereSQL = buildWhereSQL(dbType, filterConditions); const countSql = `SELECT COUNT(*) as total FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; let sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; sql += buildOrderBySQL(dbType, sortInfo, pkColumns); const offset = (page - 1) * size; // 大表性能:打开表不阻塞在 COUNT(*),先通过多取 1 条判断是否还有下一页;总数在后台统计并异步回填。 sql += ` LIMIT ${size + 1} OFFSET ${offset}`; const requestStartTime = Date.now(); let executedSql = sql; try { const executeDataQuery = async (querySql: string, attemptLabel: string) => { const startTime = Date.now(); const result = await DBQuery(config as any, dbName, querySql); addSqlLog({ id: `log-${Date.now()}-data`, timestamp: Date.now(), sql: querySql, status: result.success ? 'success' : 'error', duration: Date.now() - startTime, message: result.success ? '' : `${attemptLabel}: ${result.message}`, affectedRows: Array.isArray(result.data) ? result.data.length : undefined, dbName }); return result; }; const hasSort = !!sortInfo?.columnKey && (sortInfo?.order === 'ascend' || sortInfo?.order === 'descend'); const isSortMemoryErr = (msg: string) => /error\s*1038|out of sort memory/i.test(String(msg || '')); let resData = await executeDataQuery(sql, '主查询'); if (!resData.success && isMySQLFamily && hasSort && isSortMemoryErr(resData.message)) { const retrySql32MB = withSortBufferTuningSQL(dbType, sql, 32 * 1024 * 1024); if (retrySql32MB !== sql) { executedSql = retrySql32MB; resData = await executeDataQuery(retrySql32MB, '重试(32MB sort_buffer)'); } if (!resData.success && isSortMemoryErr(resData.message)) { const retrySql128MB = withSortBufferTuningSQL(dbType, sql, 128 * 1024 * 1024); if (retrySql128MB !== executedSql) { executedSql = retrySql128MB; resData = await executeDataQuery(retrySql128MB, '重试(128MB sort_buffer)'); } } if (resData.success) { message.warning('已自动提升排序缓冲并重试成功。'); } } if (pkColumns.length === 0) { const pkKey = `${tab.connectionId}|${dbName}|${tableName}`; if (pkKeyRef.current !== pkKey) { pkKeyRef.current = pkKey; const pkSeq = ++pkSeqRef.current; DBGetColumns(config as any, dbName, tableName) .then((resCols: any) => { if (pkSeqRef.current !== pkSeq) return; if (pkKeyRef.current !== pkKey) return; if (!resCols?.success) return; const pks = (resCols.data as ColumnDefinition[]).filter((c: any) => c.key === 'PRI').map((c: any) => c.name); setPkColumns(pks); }) .catch(() => { if (pkSeqRef.current !== pkSeq) return; if (pkKeyRef.current !== pkKey) return; }); } } if (resData.success) { let resultData = resData.data as any[]; if (!Array.isArray(resultData)) resultData = []; const hasMore = resultData.length > size; if (hasMore) resultData = resultData.slice(0, size); let fieldNames = resData.fields || []; if (fieldNames.length === 0 && resultData.length > 0) { fieldNames = Object.keys(resultData[0]); } if (fetchSeqRef.current !== seq) return; setColumnNames(fieldNames); resultData.forEach((row: any, i: number) => { if (row && typeof row === 'object') row[GONAVI_ROW_KEY] = `row-${offset + i}`; }); setData(resultData); const countKey = `${tab.connectionId}|${dbName}|${tableName}|${whereSQL}`; const derivedTotalKnown = !hasMore; const derivedTotal = derivedTotalKnown ? offset + resultData.length : page * size + 1; if (derivedTotalKnown) countKeyRef.current = countKey; setPagination(prev => { if (derivedTotalKnown) { return { ...prev, current: page, pageSize: size, total: derivedTotal, totalKnown: true }; } if (prev.totalKnown && countKeyRef.current === countKey) { return { ...prev, current: page, pageSize: size }; } return { ...prev, current: page, pageSize: size, total: derivedTotal, totalKnown: false }; }); if (!derivedTotalKnown) { if (countKeyRef.current !== countKey) { countKeyRef.current = countKey; const countSeq = ++countSeqRef.current; const countStart = Date.now(); // 大表 COUNT(*) 可能非常慢,且在部分运行时环境下会影响后续操作响应; // 这里为统计请求设置更短的超时,避免“后台统计”长期占用资源。 const countConfig: any = { ...(config as any), timeout: 5 }; DBQuery(countConfig, dbName, countSql) .then((resCount: any) => { const countDuration = Date.now() - countStart; addSqlLog({ id: `log-${Date.now()}-count`, timestamp: Date.now(), sql: countSql, status: resCount.success ? 'success' : 'error', duration: countDuration, message: resCount.success ? '' : resCount.message, dbName }); if (countSeqRef.current !== countSeq) return; if (countKeyRef.current !== countKey) return; if (!resCount.success) return; if (!Array.isArray(resCount.data) || resCount.data.length === 0) return; const total = Number(resCount.data[0]?.['total']); if (!Number.isFinite(total) || total < 0) return; setPagination(prev => ({ ...prev, total, totalKnown: true })); }) .catch(() => { if (countSeqRef.current !== countSeq) return; if (countKeyRef.current !== countKey) return; // 统计失败不影响主流程,不弹窗;可在日志里查看。 }); } } } else { message.error(String(resData.message || '查询失败')); } } catch (e: any) { if (fetchSeqRef.current !== seq) return; message.error("Error fetching data: " + e.message); addSqlLog({ id: `log-${Date.now()}-error`, timestamp: Date.now(), sql: executedSql, status: 'error', duration: Date.now() - requestStartTime, message: e.message, dbName }); } if (fetchSeqRef.current === seq) setLoading(false); }, [connections, tab, sortInfo, filterConditions, pkColumns]); // 依赖 pkColumns:在无手动排序时可回退到主键稳定排序。 // 主键信息只会在首次加载后更新一次,避免循环查询。 // Handlers memoized const handleReload = useCallback(() => { fetchData(pagination.current, pagination.pageSize); }, [fetchData, pagination.current, pagination.pageSize]); const handleSort = useCallback((field: string, order: string) => { const normalizedOrder = order === 'ascend' || order === 'descend' ? order : ''; const normalizedField = String(field || '').trim(); if (!normalizedField || !normalizedOrder) { setSortInfo(null); return; } setSortInfo({ columnKey: normalizedField, order: normalizedOrder }); }, []); const handlePageChange = useCallback((page: number, size: number) => fetchData(page, size), [fetchData]); const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []); const handleApplyFilter = useCallback((conditions: FilterCondition[]) => setFilterConditions(conditions), []); useEffect(() => { fetchData(1, pagination.pageSize); }, [tab, sortInfo, filterConditions]); // Initial load and re-load on sort/filter return (
); }; export default DataViewer;