Repository navigation
fix(index): give every NaN one index key, below -Infinity - #327
Open
CaffeinatedCoder wants to merge 2 commits into
Open
CaffeinatedCoder wants to merge 2 commits into
CaffeinatedCoder wants to merge 2 commits into
Conversation
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>
… one key A NaN with the sign bit clear (what Arm64 computes) was keyed above +Infinity; the canonical NaN key no longer reaches that entry, so lookups missed it and deleting its document left it behind. NeedsKeyFormatMigration now also looks for keys above +Infinity in double- and float-keyed indexes, as it does for the former -0.0 key (EntglDb#249), and the index is rebuilt on open. Tests: IndexKeyNaNTests.Index_Holding_The_Former_Positive_NaN_Key_Is_Rebuilt_On_Open fails without the change; full suite 3104/0/4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder
had a problem deploying
to
Production
October 19, 2026 04:42 — with
GitHub Actions
Failure
This branch had an error being 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.
Fixes #257
Supersedes #282, whose head branch lives on EntglDb and can no longer be updated by the author. This is #282's commit rebased onto
main(#252 is merged), plus one commit for the review item on #282.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) 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.Review item from #282: migrating keys already stored
Positive-NaN keys already stored (on Arm64) sat under the old above-
+Infinitykey with no migration, so lookups missed them and deleting the document left a stale entry.NeedsKeyFormatMigrationnow also looks for any key above that of+Infinityin a double- or float-keyed index, the same way #252 looks for the former-0.0key, and such an index is rebuilt on open. This costs one more seek per index and open. No KeyFormat bump: the encoding of every non-NaN double is unchanged.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. Four of the five cases fail without the fix; thedouble.NaNrow already had this key.Index_Orders_NaNs_First_And_Excludes_Them_From_A_Lower_Bounded_Range, onBTreeIndex: ascending order is NaNs first, then 1.0 and+Inf;>= 0.5returns 1.0 and+Infonly;>= double.MaxValuereturns+Infonly.Index_Holding_The_Former_Positive_NaN_Key_Is_Rebuilt_On_Open(new): writes the former key of a positive NaN into adoubleindex, reopens, and checks the index was rebuilt with the canonical NaN key, that deleting the document removes its entry, and that an index holding+Infinitybut no former NaN key is not rebuilt. It fails without the migration change.dotnet test tests/BLite.Testson the #282 base (with #252): 3104 passed, 0 failed, 4 skipped. Onmainafter the rebase: 3176 passed, 1 failed, 4 skipped. The failure wasIndexPageReleaseTests.CreateDropIndexCycles_Typed_DoNotGrowTheFile, which passed 3 of 3 times when run on its own. A second full run on a heavily loaded machine (load average 28) failed in 7 unrelated tests, all withTimed out acquiring PageFile write lock, a different set each run (#303).Compatibility
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.double.NaN's also had a key apart from the canonical one. Such NaNs only come from explicitly constructed bits, and the migration does not look for them.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