From 39fcaa15cb4b14f30b6c6b660d91a071c8607138 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 5 Aug 2026 09:40:01 +0800 Subject: [PATCH] fix(grid): sort negative values correctly on current page --- apps/desktop/src/lib/dataGrid/dataGridSort.ts | 64 +++++++++++++++++-- apps/desktop/src/stores/queryStore.ts | 3 +- packages/app-tests/dataGridSort.test.ts | 7 ++ packages/app-tests/queryStore.test.ts | 25 +++++++- 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/lib/dataGrid/dataGridSort.ts b/apps/desktop/src/lib/dataGrid/dataGridSort.ts index c080e05a5..b37eda65b 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridSort.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridSort.ts @@ -1,3 +1,5 @@ +import { isNumericColumnType } from "@/lib/dataGrid/dataGridColumnType"; + export type DataGridSortDirection = "asc" | "desc"; export type DataGridSortMode = "database" | "local"; @@ -60,25 +62,25 @@ type DataGridRow = DataGridCellValue[]; const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); -export function sortDataGridRows(rows: readonly T[], columnIndex: number, direction: DataGridSortDirection): T[] { - return sortDataGridRowIndexes(rows, columnIndex, direction).map((index) => rows[index]!); +export function sortDataGridRows(rows: readonly T[], columnIndex: number, direction: DataGridSortDirection, columnType?: string): T[] { + return sortDataGridRowIndexes(rows, columnIndex, direction, columnType).map((index) => rows[index]!); } -export function sortDataGridRowIndexes(rows: readonly DataGridRow[], columnIndex: number, direction: DataGridSortDirection): number[] { +export function sortDataGridRowIndexes(rows: readonly DataGridRow[], columnIndex: number, direction: DataGridSortDirection, columnType?: string): number[] { const directionMultiplier = direction === "asc" ? 1 : -1; return rows .map((row, index) => ({ row, index })) .sort((left, right) => { const emptyCompared = compareEmptyValues(left.row[columnIndex], right.row[columnIndex]); if (emptyCompared !== null) return emptyCompared; - const compared = compareDataGridValues(left.row[columnIndex], right.row[columnIndex]); + const compared = compareDataGridValues(left.row[columnIndex], right.row[columnIndex], columnType); if (compared !== 0) return compared * directionMultiplier; return left.index - right.index; }) .map((item) => item.index); } -export function compareDataGridValues(left: DataGridCellValue, right: DataGridCellValue): number { +export function compareDataGridValues(left: DataGridCellValue, right: DataGridCellValue, columnType?: string): number { const leftEmpty = left == null; const rightEmpty = right == null; if (leftEmpty || rightEmpty) { @@ -86,6 +88,11 @@ export function compareDataGridValues(left: DataGridCellValue, right: DataGridCe return leftEmpty ? 1 : -1; } + if (isNumericColumnType(columnType)) { + const numericCompared = compareNumericCellValues(left, right); + if (numericCompared !== null) return numericCompared; + } + if (typeof left === "number" && typeof right === "number") { return compareNumbers(left, right); } @@ -102,6 +109,53 @@ export function compareDataGridValues(left: DataGridCellValue, right: DataGridCe return collator.compare(String(left), String(right)); } +interface NumericSortValue { + sign: -1 | 0 | 1; + magnitude: bigint; + digits: string; +} + +function compareNumericCellValues(left: DataGridCellValue, right: DataGridCellValue): number | null { + const leftNumber = parseNumericSortValue(left); + const rightNumber = parseNumericSortValue(right); + if (!leftNumber || !rightNumber) return null; + if (leftNumber.sign !== rightNumber.sign) return leftNumber.sign - rightNumber.sign; + if (leftNumber.sign === 0) return 0; + + let compared = compareBigInts(leftNumber.magnitude, rightNumber.magnitude); + if (compared === 0) { + const width = Math.max(leftNumber.digits.length, rightNumber.digits.length); + compared = leftNumber.digits.padEnd(width, "0").localeCompare(rightNumber.digits.padEnd(width, "0")); + } + return leftNumber.sign === 1 ? compared : -compared; +} + +function parseNumericSortValue(value: DataGridCellValue): NumericSortValue | null { + if (typeof value !== "string" && typeof value !== "number") return null; + const text = String(value).trim(); + const negative = text.startsWith("-"); + const unsigned = /^[+-]/.test(text) ? text.slice(1) : text; + const match = unsigned.match(/^(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/); + if (!match) return null; + + const integerDigits = match[1] ?? ""; + const fractionDigits = match[2] ?? ""; + const allDigits = `${integerDigits}${fractionDigits}`; + if (!allDigits) return null; + const leadingZeroCount = allDigits.match(/^0*/)?.[0].length ?? 0; + const digits = allDigits.slice(leadingZeroCount); + if (!digits) return { sign: 0, magnitude: 0n, digits: "0" }; + + const exponent = BigInt(match[3] ?? "0"); + const magnitude = BigInt(integerDigits.length - leadingZeroCount) + exponent; + return { sign: negative ? -1 : 1, magnitude, digits }; +} + +function compareBigInts(left: bigint, right: bigint): number { + if (left === right) return 0; + return left < right ? -1 : 1; +} + function compareEmptyValues(left: DataGridCellValue, right: DataGridCellValue): number | null { const leftEmpty = left == null; const rightEmpty = right == null; diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 8798cf199..cb0316072 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -1159,7 +1159,8 @@ export const useQueryStore = defineStore("query", () => { } const originalRows = tab.resultLocalSortOriginalRows; - const rowIndexes = direction ? sortDataGridRowIndexes(originalRows, columnIndex, direction) : originalRows.map((_, index) => index); + const columnType = tab.result.column_types?.[columnIndex]; + const rowIndexes = direction ? sortDataGridRowIndexes(originalRows, columnIndex, direction, columnType) : originalRows.map((_, index) => index); const rows = rowIndexes.map((index) => originalRows[index]!); const originalMongoDocuments = tab.resultLocalSortOriginalMongoDocuments; const mongo_documents = originalMongoDocuments ? rowIndexes.map((index) => originalMongoDocuments[index]) : undefined; diff --git a/packages/app-tests/dataGridSort.test.ts b/packages/app-tests/dataGridSort.test.ts index b85c2b66d..44b85755c 100644 --- a/packages/app-tests/dataGridSort.test.ts +++ b/packages/app-tests/dataGridSort.test.ts @@ -38,6 +38,13 @@ test("sortDataGridRows uses natural string order and keeps equal values stable", ]); }); +test("sortDataGridRows sorts numeric strings by signed value for numeric columns", () => { + const rows = [["-27700"], ["-78800"], ["297500"], ["9007199254740993"], ["9007199254740992"], ["-1.2e3"], ["-1.19e3"], ["1.01"], ["1.001"]]; + + assert.deepEqual(sortDataGridRows(rows, 0, "asc", "NUMBER"), [["-78800"], ["-27700"], ["-1.2e3"], ["-1.19e3"], ["1.001"], ["1.01"], ["297500"], ["9007199254740992"], ["9007199254740993"]]); + assert.deepEqual(sortDataGridRows([["-27700"], ["-78800"]], 0, "asc", "VARCHAR2"), [["-27700"], ["-78800"]]); +}); + test("sortDataGridRows sorts ISO date strings by time", () => { const rows = [["2026-02-01"], ["2025-12-31"], ["2026-01-01"]]; diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index af5dca3b2..17e6dde77 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -1175,6 +1175,26 @@ test("sortTabResultLocally sorts current rows and restores original order", () = assert.equal(tab.resultLocalSortOriginalMongoCopyDocuments, undefined); }); +test("sortTabResultLocally uses result column types for numeric strings", () => { + setActivePinia(createPinia()); + const store = useQueryStore(); + const tabId = store.createTab("conn-1", "db"); + const tab = store.tabs.find((item) => item.id === tabId); + assert.ok(tab); + + tab.result = { + columns: ["QUANTITY_IN_STOCK"], + column_types: ["NUMBER"], + rows: [["-27700"], ["-78800"], ["297500"]], + affected_rows: 0, + execution_time_ms: 1, + }; + + store.sortTabResultLocally(tabId, "QUANTITY_IN_STOCK", 0, "asc"); + + assert.deepEqual(tab.result.rows, [["-78800"], ["-27700"], ["297500"]]); +}); + test("selecting a result run restores its displayed result without changing SQL draft", async () => { setActivePinia(createPinia()); const store = useQueryStore(); @@ -4674,7 +4694,10 @@ test("mongo dropIndexes execution exposes partial failures and refreshes loaded assert.equal(indexRefreshRequested, true); const indexGroup = connectionStore.treeNodes[0]?.children?.[0]?.children?.[0]?.children?.[0]; assert.equal(indexGroup?.isExpanded, false); - assert.deepEqual(indexGroup?.children?.map((node) => node.label), ["_id_ (_id)"]); + assert.deepEqual( + indexGroup?.children?.map((node) => node.label), + ["_id_ (_id)"], + ); } finally { globalThis.fetch = originalFetch; restoreStorage();