From 56deb2e8883f9c879caf3b4d8b5850461893d967 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Mon, 3 Aug 2026 14:05:26 -0700 Subject: [PATCH] =?UTF-8?q?fix(namespace):=20the=20JS=20sorted=20fallback?= =?UTF-8?q?=20honors=20the=20ruled=20ordering=20contract=20=E2=80=94=20nul?= =?UTF-8?q?ls=20last=20in=20BOTH=20directions=20(was=20nulls-first=20on=20?= =?UTF-8?q?desc)=20+=20deterministic=20id-ascending=20tie-break?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/metadataIndex.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/utils/metadataIndex.ts b/src/utils/metadataIndex.ts index fdb17c22..cf5d521e 100644 --- a/src/utils/metadataIndex.ts +++ b/src/utils/metadataIndex.ts @@ -2260,20 +2260,30 @@ export class MetadataIndexManager implements MetadataIndexProvider { } idValuePairs.sort((a, b) => { - if (a.value == null && b.value == null) return 0 - if (a.value == null) return order === 'asc' ? 1 : -1 - if (b.value == null) return order === 'asc' ? -1 : 1 - if (a.value === b.value) return 0 + // Ordering contract (cross-engine, ruled 2026-08-03): missing/null + // values sort LAST in BOTH directions — the direction flip never moves + // them to the front — and ties break by id ascending, so an ordered + // read is deterministic and identical on both engines. Rows are never + // dropped for lacking the field. + const aNull = a.value == null + const bNull = b.value == null + if (aNull || bNull) { + if (aNull && bNull) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 + return aNull ? 1 : -1 + } // Numbers compare numerically; everything else by code-point (UTF-8 byte) order. // This makes the JS fallback sort match cor's native column store exactly // (numeric i64/f64 vs code-point strings) and stay deterministic across // environments, unlike the `<` operator's UTF-16 ordering for strings. - let comparison: number - if (typeof a.value === 'number' && typeof b.value === 'number') { - comparison = a.value < b.value ? -1 : 1 - } else { - comparison = compareCodePoints(String(a.value), String(b.value)) + let comparison = 0 + if (a.value !== b.value) { + if (typeof a.value === 'number' && typeof b.value === 'number') { + comparison = a.value < b.value ? -1 : 1 + } else { + comparison = compareCodePoints(String(a.value), String(b.value)) + } } + if (comparison === 0) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 return order === 'asc' ? comparison : -comparison })