fix(grid): sort negative values correctly on current page

This commit is contained in:
zipg 2026-08-05 09:40:01 +08:00 committed by GitHub
parent c3547fcb61
commit 39fcaa15cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 7 deletions

View File

@ -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<T extends DataGridRow>(rows: readonly T[], columnIndex: number, direction: DataGridSortDirection): T[] {
return sortDataGridRowIndexes(rows, columnIndex, direction).map((index) => rows[index]!);
export function sortDataGridRows<T extends DataGridRow>(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;

View File

@ -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;

View File

@ -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"]];

View File

@ -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();