fix(grid): parse pg timestamps deterministically for local date sort
Closes #7281
This commit is contained in:
2 files changed
+54
-4
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { simpleDataGridOrderByColumn, simpleDataGridOrderByMatchesSort, simpleDataGridOrderByReferencesMissingColumn } from "@/lib/dataGrid/dataGridSort";
|
||||
import { compareDataGridValues, simpleDataGridOrderByColumn, simpleDataGridOrderByMatchesSort, simpleDataGridOrderByReferencesMissingColumn } from "@/lib/dataGrid/dataGridSort";
|
||||
|
||||
describe("simpleDataGridOrderByColumn", () => {
|
||||
it.each([
|
||||
@@ -46,3 +46,36 @@ describe("simpleDataGridOrderByMatchesSort", () => {
|
||||
expect(simpleDataGridOrderByMatchesSort('"name" ASC', "old_name", "asc")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareDataGridValues datetime ordering", () => {
|
||||
it("orders space-separated timestamps with microsecond precision", () => {
|
||||
expect(compareDataGridValues("2024-01-01 12:00:00.123456", "2024-01-01 12:00:00.500000")).toBeLessThan(0);
|
||||
expect(compareDataGridValues("2024-01-01 12:00:00.500000", "2024-01-01 12:00:00.123456")).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("orders dates before earlier-day late times across midnight", () => {
|
||||
expect(compareDataGridValues("2024-01-02 00:00:00", "2024-01-01 23:59:59")).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("normalizes numeric UTC offsets so equal instants compare equal", () => {
|
||||
expect(compareDataGridValues("2024-01-01 12:00:00+08", "2024-01-01 13:00:00+09")).toBe(0);
|
||||
expect(compareDataGridValues("2024-01-01 11:00:00+09", "2024-01-01 12:00:00+08")).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("sorts PostgreSQL infinity sentinels beyond finite timestamps", () => {
|
||||
expect(compareDataGridValues("infinity", "9999-12-31 23:59:59")).toBeGreaterThan(0);
|
||||
expect(compareDataGridValues("-infinity", "0001-01-01 00:00:00")).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("still parses ISO-T timestamps with Zulu suffix", () => {
|
||||
expect(compareDataGridValues("2024-01-01T12:00:00Z", "2024-01-01T12:00:01Z")).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("falls back to collation for non-datetime strings", () => {
|
||||
expect(compareDataGridValues("zebra", "apple")).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps null values after populated datetime cells", () => {
|
||||
expect(compareDataGridValues(null, "2024-01-01 00:00:00")).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -173,7 +173,24 @@ function compareNumbers(left: number, right: number): number {
|
||||
}
|
||||
|
||||
function dateSortValue(value: string): number | null {
|
||||
if (!/^\d{4}-\d{2}-\d{2}/.test(value)) return null;
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
const trimmed = value.trim();
|
||||
// PostgreSQL and KingbaseES render timestamps as `YYYY-MM-DD HH:MM:SS.ffffff`
|
||||
// (space separator, microsecond precision, optional numeric UTC offset) plus
|
||||
// the special values infinity/-infinity. Date.parse rejects several of those
|
||||
// shapes depending on the engine, silently degrading time columns to
|
||||
// lexicographic order (issue #7281), so parse them deterministically.
|
||||
if (trimmed === "infinity") return Number.MAX_SAFE_INTEGER;
|
||||
if (trimmed === "-infinity") return Number.MIN_SAFE_INTEGER;
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}(?::?\d{2})?)?)?/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
const [, year, month, day, hour = "0", minute = "0", second = "0", fraction, zone] = match;
|
||||
let sortValue = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second)) + (fraction ? Number(`0.${fraction}`) * 1000 : 0);
|
||||
if (zone && zone !== "Z") {
|
||||
const zoneMatch = /^([+-])(\d{2}):?(\d{2})?$/.exec(zone);
|
||||
if (zoneMatch) {
|
||||
const offsetMinutes = Number(zoneMatch[2]) * 60 + Number(zoneMatch[3] ?? 0);
|
||||
sortValue -= (zoneMatch[1] === "-" ? -1 : 1) * offsetMinutes * 60_000;
|
||||
}
|
||||
}
|
||||
return Number.isFinite(sortValue) ? sortValue : null;
|
||||
}
|
||||
Reference in new issue
Block a user