A NaN with the sign bit clear sorts above +Infinity in an index on a double
Maintainers usually reply within 2 days
@CaffeinatedCoder is already working on this.
Since Oct 17, 2026.
Assessment
- Difficulty
- 4/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 54/100
Research direction
Start with src/BLite.Core/Indexing/IndexKey.cs lines 94–105 and the B-tree model test at tests/BLite.Tests/Oracles/BTreeModelTests.cs. Check how double keys handle both NaN sign bits and how indexed range counts compare with unindexed results. Done when NaNs sort consistently below numbers, comparisons and counts match the stated expectations, and existing index rebuild needs are addressed.
Written by the indexing model from the issue text.
Description
IndexKey(double) gives a NaN with the sign bit clear a key above +Infinity, and a NaN with the sign bit set (the constant double.NaN) a key below -Infinity. On Arm64 every NaN the program computes has the sign bit clear, so index-served orderings put those documents last and index-served counts with a lower bound include them. On x64, where computed NaNs carry the sign bit, the same documents sort first: the order of a query depends on the CPU that wrote the value.
Found by the B-tree model test (tests/BLite.Tests/Oracles/BTreeModelTests.cs, #259), which carries the affected rows as a known candidate until this is fixed.
Sibling of #249 (-0.0 and +0.0 get different keys): the same encoding, whose text names NaN as a separate question; this is that question. #252 leaves NaN out of scope. If #254 is fixed by keying dynamic doubles with IndexKey(double), DynamicCollection inherits this too.
Repro
double zero = double.Parse("0");
double computed = zero / zero; // 0x7FF8000000000000 on Arm64; double.NaN is 0xFFF8000000000000
IndexKey.Create(computed) > IndexKey.Create(double.PositiveInfinity); // true
IndexKey.Create(computed) == IndexKey.Create(double.NaN); // false
// DiffDoc has double Ratio with .HasIndex(x => x.Ratio).
await ctx.Docs.InsertBulkAsync([
new DiffDoc { Name = "d1", Ratio = computed },
new DiffDoc { Name = "d2", Ratio = 1.0 },
new DiffDoc { Name = "d3", Ratio = double.NaN }]);
ctx.Docs.AsQueryable().OrderBy(x => x.Ratio).Take(3); // expected d1, d3, d2; actual d3, d2, d1
ctx.Docs.AsQueryable().OrderByDescending(x => x.Ratio).Take(3); // expected d2, d1, d3; actual d1, d2, d3
ctx.Docs.AsQueryable().Count(x => x.Ratio > 0.5); // expected 1; actual 2
ctx.Docs.AsQueryable().Where(x => x.Ratio >= double.MaxValue); // expected []; actual [d1]
Expected: double.CompareTo, which LINQ-to-Objects and the unindexed collection use, orders every NaN below every number and equal to every other NaN; every comparison operator with a NaN operand is false in C#.
Actual: on Arm64, every computed NaN (0.0 / 0.0, Math.Sqrt(-1), ∞ - ∞ all give 0x7FF8000000000000) sorts above +Infinity, while double.NaN sorts below -Infinity.
Mechanism (verified on 0450b97)
IndexKey(double)(src/BLite.Core/Indexing/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 is arbitrary, so the two NaN halves land below-Infinity(sign set) and above+Infinity(sign clear), and every distinct payload is a distinct key.Where(x => x.Ratio > 0.5)itself returns the right answer because it is post-filtered with the BSON predicate;Countis answered from the index (CountRange) and is not.
Affected versions: since the sort-preserving double encoding in f83e0d5 (2026-03-17, first in v3.7.0). Reproduced on v5.2.3 / 0450b97 on Arm64; x64 not run.
Fix considerations
- Canonicalise in
IndexKey(double)together with the-0.0fix of #249:if (double.IsNaN(value)) value = double.NaN;before taking the bits gives every NaN the one key below-Infinity, which is wheredouble.CompareToputs it. - Range bounds then exclude NaN from
> xand>= x. Whether< xshould include it (C#: false,CompareTo: NaN is smallest) is the same question the index already answers for-Infinity; the BSON post-filter decides forWhere, not forCount. - Existing indexes with a positive-NaN key need a rebuild.
- Dominant language
- C#
- Stars
- 66
- Forks
- 6
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 58
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from EntglDb/BLite
-
GdprMode.Strict warns that deleted documents are not zeroed, but every delete zero-fills themPossibly taken @CaffeinatedCoder claimed this 3 days ago. Openbug gdpr package/core severity/low
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
EntglDb/BLite#299 · 1 comment ·
Maintainers usually reply within 2 days
-
bug package/core severity/medium triage
Difficulty 4/5 3-5 days Newbie friendliness 54/100
Maintainers usually reply within 2 days
-
bug package/core severity/medium
Difficulty 4/5 3-5 days Newbie friendliness 48/100
EntglDb/BLite#322 · 1 comment ·
Maintainers usually reply within 2 days
-
enhancement package/core
Difficulty 4/5 3-5 days Newbie friendliness 48/100
EntglDb/BLite#320 · 1 comment ·
Maintainers usually reply within 2 days
-
BLiteSession.Rollback() does not await the rollback: it returns while the transaction is still active, and failures go unobservedPossibly taken @CaffeinatedCoder claimed this 3 days ago. Openbug package/core severity/medium
Difficulty 3/5 1-2 days Newbie friendliness 78/100
EntglDb/BLite#307 · 1 comment ·
Maintainers usually reply within 2 days
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
bug priority: P2
Difficulty 1/5 Under an hour Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
IridiumIO/CompactGUI#699 ·
Maintainers usually reply within 3 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day