Repository navigation
fix(index): give every NaN one index key, below -Infinity - #282
Open
CaffeinatedCoder wants to merge 1 commit into
Open
CaffeinatedCoder wants to merge 1 commit into
CaffeinatedCoder wants to merge 1 commit into
Conversation
This was referenced Oct 17, 2026
IndexKey(double) flips all bits of a value with the sign bit set and only the sign bit otherwise. A NaN's sign bit and payload are arbitrary, so a NaN with the sign bit clear (what Arm64 computes for 0.0 / 0.0, Sqrt(-1) or Inf - Inf) encoded above +Infinity while double.NaN (sign bit set, what x64 computes) encoded below -Infinity, and every payload was a distinct key. Index-served orderings then put documents holding a computed NaN last on Arm64 and first on x64, and index-served counts with a lower bound (Count(x => x.Ratio > 0.5) through CountRange) included them. Every NaN now takes the key of the bits 0xFFF8000000000000 (double.NaN), spelled out so it does not depend on how the NaN was produced. That key sorts below -Infinity, where double.CompareTo puts NaN, and a range with a finite lower bound no longer reaches it. Existing indexes holding a NaN with the sign bit clear keep its old key until they are rebuilt. Found-By: OT Defect-Class: IDX Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder
force-pushed
the
fix/index-key-nan-canonical
branch
from
October 18, 2026 15:47
f41339f to
ea52725
Compare
CaffeinatedCoder
changed the base branch from
main
to
fix/249-negative-zero-index-key
October 18, 2026 15:48
mrdevrobot
requested changes
Oct 18, 2026
mrdevrobot
left a comment
Contributor
There was a problem hiding this comment.
Changes requested. The fix is right, but positive-NaN keys already stored on Arm64 stay under the old key with no migration: lookups miss them and deleting the document leaves a stale index entry. Detect the old above-+Infinity NaN key in NeedsKeyFormatMigration, as #252 does for -0.0, so affected indexes rebuild on open.
Collaborator
Author
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #252 (base
fix/249-negative-zero-index-key): it lands after #252. Rebased onto #252's head8a6f320, soIndexKey(double)has both changes: a zero becomes +0.0 first, then every NaN takes the key ofdouble.NaN. I will retarget it tomainonce #252 merges. The diff here is the one commit on top of #252.What was wrong
IndexKey(double)gave a NaN with the sign bit clear a key above+Infinity, and a NaN with the sign bit set (the constantdouble.NaN) a key below-Infinity(#257). Every computed NaN on Arm64 (0.0 / 0.0,Math.Sqrt(-1),∞ - ∞) has the sign bit clear; on x64 it has it set. So index-served orderings put those documents last on one CPU and first on the other, and index-served counts with a lower bound (Count(x => x.Ratio > 0.5)throughCountRange) included them.Mechanism
The sort-preserving encoding (
IndexKey.cs:94-105) flips all bits of a value with the sign bit set and only the sign bit otherwise. A NaN's sign bit and payload are arbitrary, so the two NaN halves landed below-Infand above+Inf, and every payload got a distinct key.Fix
Every NaN now takes one key: that of the bits
0xFFF8000000000000(double.NaN), spelled out as a constant so the key does not depend on how the NaN was produced. That key sorts below-Infinity, which is wheredouble.CompareTo(and so LINQ-to-Objects and the unindexed path) puts NaN. A range with a finite lower bound no longer reaches it.Range semantics: NaN is now the smallest double key, as
-Infinitywas before.< xserved from the index includes NaN, the same way it includes-Infinity. Whether< xshould exclude NaN (C# comparison operators say false) is the open question the issue names; the BSON post-filter decides forWhere, not forCount. I left that as it is.Tests
IndexKeyNaNTests:Every_NaN_Has_The_Key_Of_Double_NaN_Below_Negative_Infinity, a theory over a positive NaN (0x7FF8…), a positive payload NaN, a negative payload NaN anddouble.NaN. Each must equalIndexKey.Create(double.NaN), sort below-InfinityandMinValue, and round-trip to a NaN.Index_Orders_NaNs_First_And_Excludes_Them_From_A_Lower_Bounded_Range, onBTreeIndex: the issue'sd1(computed NaN),d2(1.0) andd3(double.NaN) plus+Inf. Ascending order is NaNs first, then 1.0 and+Inf;>= 0.5returns 1.0 and+Infonly;>= double.MaxValuereturns+Infonly.Four of the five cases fail on
upstream/main(checked by revertingsrc/withgit checkout HEAD~1 -- src); thedouble.NaNrow passes there, since that NaN already had this key. All pass with the fix.dotnet test tests/BLite.Tests: 3095 passed, 4 skipped. Two tests failed under load from parallel runs (SpatialVectorIndexMaintenanceTests.Vector_*,BTreeSplitTests.Dynamic_*). Both classes pass when rerun on their own (the spatial class 4 runs out of 4), and the spatial index does not useIndexKey.Compatibility
double(ordouble?) property that holds a NaN with the sign bit clear keeps the old above-+Infkey until it is rebuilt. A lookup for that NaN then misses it, and a delete of that document leaves its entry. Drop and recreate such indexes, or runVacuumAsync. Indexes without NaNs, or with onlydouble.NaN-patterned NaNs (x64), are unaffected.-0.0, Indexed double equality misses -0.0: IndexKey(double) gives the two zeros different keys #249) adds a line at the same spot inIndexKey(double). The two changes are independent, but the second to merge will need a trivial rebase. Done: this PR is now stacked on fix(index): -0.0 and +0.0 share one index key (#249) #252. On the combined branch,SignedZeroIndexTestsandIndexKeyNaNTests(11 tests) and the fulldotnet test tests/BLite.Testspass: 3103 passed, 0 failed, 4 skipped.DynamicCollectiondouble keys (DynamicCollection: range filters and sorts on an indexed double field return wrong results (keys are little-endian BitConverter bytes) #254) do not go throughIndexKey(double)today. If DynamicCollection: range filters and sorts on an indexed double field return wrong results (keys are little-endian BitConverter bytes) #254 is fixed that way, they inherit this.Fixes #257
The B-tree model test PR (#259) lists this as a known candidate. Its registry entry can go once this lands.
🤖 Generated with Claude Code