diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 0f8f4fe..a7661c0 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -5b8157374dae5f9340e31b2d0bd2c00e \ No newline at end of file +d0f9366af59a6367ad3c7e2d4185ead4 \ No newline at end of file diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 7a1a604..6bf7aaa 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -9,7 +9,7 @@ import ImportPreviewModal from './ImportPreviewModal'; import { useStore } from '../store'; import { v4 as uuidv4 } from 'uuid'; import 'react-resizable/css/styles.css'; -import { buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; +import { buildOrderBySQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; import { isMacLikePlatform, normalizeOpacityForPlatform } from '../utils/appearance'; // --- Error Boundary --- @@ -1819,13 +1819,11 @@ const DataGrid: React.FC = ({ if (!tableName || !pagination) return ''; const whereSQL = buildWhereSQL(dbType, filterConditions); let sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; - if (sortInfo && sortInfo.order) { - sql += ` ORDER BY ${quoteIdentPart(dbType, sortInfo.columnKey)} ${sortInfo.order === 'ascend' ? 'ASC' : 'DESC'}`; - } + sql += buildOrderBySQL(dbType, sortInfo, pkColumns); const offset = (pagination.current - 1) * pagination.pageSize; sql += ` LIMIT ${pagination.pageSize} OFFSET ${offset}`; return sql; - }, [tableName, pagination, filterConditions, sortInfo]); + }, [tableName, pagination, filterConditions, sortInfo, pkColumns]); // Context Menu Export const handleExportSelected = useCallback(async (format: string, record: any) => { diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx index 1ca4b8c..cdc132d 100644 --- a/frontend/src/components/DataViewer.tsx +++ b/frontend/src/components/DataViewer.tsx @@ -4,7 +4,7 @@ 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 { buildWhereSQL, quoteIdentPart, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; +import { buildOrderBySQL, buildWhereSQL, quoteQualifiedIdent, type FilterCondition } from '../utils/sql'; const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const [data, setData] = useState([]); @@ -69,9 +69,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const countSql = `SELECT COUNT(*) as total FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; let sql = `SELECT * FROM ${quoteQualifiedIdent(dbType, tableName)} ${whereSQL}`; - if (sortInfo && sortInfo.order) { - sql += ` ORDER BY ${quoteIdentPart(dbType, sortInfo.columnKey)} ${sortInfo.order === 'ascend' ? 'ASC' : 'DESC'}`; - } + sql += buildOrderBySQL(dbType, sortInfo, pkColumns); const offset = (page - 1) * size; // 大表性能:打开表不阻塞在 COUNT(*),先通过多取 1 条判断是否还有下一页;总数在后台统计并异步回填。 sql += ` LIMIT ${size + 1} OFFSET ${offset}`; @@ -205,13 +203,9 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { }); } if (fetchSeqRef.current === seq) setLoading(false); - }, [connections, tab, sortInfo, filterConditions, pkColumns.length]); - // Depend on pkColumns.length to avoid loop? No, pkColumns is updated inside. - // Actually, 'pkColumns' state shouldn't trigger re-fetch. - // The 'if (pkColumns.length === 0)' check is inside. - // So adding pkColumns to dependency is safer but might trigger double fetch if not careful? - // Only if pkColumns changes. It changes once from [] to [...]. - // So it's fine. + }, [connections, tab, sortInfo, filterConditions, pkColumns]); + // 依赖 pkColumns:在无手动排序时可回退到主键稳定排序。 + // 主键信息只会在首次加载后更新一次,避免循环查询。 // Handlers memoized const handleReload = useCallback(() => { diff --git a/frontend/src/utils/sql.ts b/frontend/src/utils/sql.ts index 6aca539..0571c99 100644 --- a/frontend/src/utils/sql.ts +++ b/frontend/src/utils/sql.ts @@ -63,6 +63,41 @@ export const quoteQualifiedIdent = (dbType: string, ident: string) => { export const escapeLiteral = (val: string) => (val || '').replace(/'/g, "''"); +type SortInfo = { + columnKey?: string; + order?: string; +} | null | undefined; + +export const buildOrderBySQL = ( + dbType: string, + sortInfo: SortInfo, + fallbackColumns: string[] = [], +) => { + const sortColumn = normalizeIdentPart(String(sortInfo?.columnKey || '')); + const sortOrder = String(sortInfo?.order || ''); + const direction = sortOrder === 'ascend' ? 'ASC' : sortOrder === 'descend' ? 'DESC' : ''; + if (sortColumn && direction) { + return ` ORDER BY ${quoteIdentPart(dbType, sortColumn)} ${direction}`; + } + + const seen = new Set(); + const stableColumns = (fallbackColumns || []) + .map((col) => normalizeIdentPart(String(col || ''))) + .filter((col) => { + if (!col) return false; + const key = col.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + if (stableColumns.length > 0) { + const parts = stableColumns.map((col) => `${quoteIdentPart(dbType, col)} ASC`); + return ` ORDER BY ${parts.join(', ')}`; + } + + return ''; +}; + export const parseListValues = (val: string) => { const raw = (val || '').trim(); if (!raw) return [];