Skip to content

Indexed double equality misses -0.0: IndexKey(double) gives the two zeros different keys #249

Closed

Description

@CaffeinatedCoder

Found by a differential test that runs ~330 LINQ shapes over the same documents on a typed collection with and without indexes and with LINQ-to-Objects as the reference (PR to follow). Two rows disagree, on the indexed collection only.

Repro

// DiffDoc has double Ratio with .HasIndex(x => x.Ratio).
await ctx.Docs.InsertBulkAsync([
    new DiffDoc { Name = "d1", Ratio = -0.0 },
    new DiffDoc { Name = "d2", Ratio = 0.0 },
    new DiffDoc { Name = "d3", Ratio = 1.0 }]);

ctx.Docs.AsQueryable().Where(x => x.Ratio == 0.0).ToList();  // expected [d1, d2], actual [d2]
ctx.Docs.AsQueryable().Count(x => x.Ratio == 0.0);           // expected 2,        actual 1

Expected: IEEE 754 equality, which C#, LINQ-to-Objects and the BSON predicate on the plain collection all use, says -0.0 == 0.0.

Actual: only the +0.0 document when the property is indexed.

Mechanism (verified on 0450b97)

IndexKey(double) is an order-preserving bit encoding (src/BLite.Core/Indexing/IndexKey.cs:94-105): negative values flip all bits, positive values flip the sign bit. -0.0 has the sign bit set and encodes to 0x7FFF…FFFF; +0.0 encodes to 0x8000…0000. Adjacent but distinct keys, and the equality plan is an exact point lookup on the key of +0.0. By the same encoding x.Ratio >= 0.0 excludes -0.0 and x.Ratio < 0.0 includes it, both unlike C# (not covered by a test row yet).

Affected versions: since the sort-preserving double encoding in f83e0d5 (2026-03-17, v3.7.0). Reproduced on v5.2.3 / 0450b97.

Fix

Normalise -0.0 to +0.0 in IndexKey(double) (if (value == 0) value = 0.0; before taking the bits), which fixes stored keys and query bounds together. Existing indexes holding a -0.0 key would need a rebuild to be found by the new bound. NaN is a separate question (C# says NaN != NaN; the encoding gives it one or several keys) and is outside this issue.

Also noticed, not verified: DynamicCollection.BsonValueToIndexKey encodes doubles with BitConverter.GetBytes (DynamicCollection.cs:2725), little-endian and not order-preserving, unlike the typed path, so range bounds on a dynamic double index may be off. The differential test covers typed collections only.

Happy to send the fix if you want it.

Activity

  1. moved this from Todo to In Progress in BLiteon Oct 16, 2026
  2. CaffeinatedCoder commented on Oct 16, 2026

    @CaffeinatedCoder
    CollaboratorAuthor

    Fix in #252: IndexKey(double) maps a zero to +0.0, and an index that still holds the former -0.0 key is rebuilt on open through the existing key-format migration (no KeyFormat bump). The dynamic-collection double encoding is not order-preserving (range queries on an indexed dynamic double return wrong results); the PR describes it, I will file it separately.

  3. added
    severity/mediumSpace leak, inconsistency or failure in a narrow scenario
    and removed on Oct 18, 2026
  4. mrdevrobot commented on Oct 18, 2026

    @mrdevrobot
    Contributor

    Reproduced on 0450b97 with the tests from #252. Edge value: medium.

  5. moved this from In Progress to Done in BLiteon Oct 18, 2026
  6. added this to the 5.3.0 milestone on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/indexingB-Tree and secondary indexes, index lifecyclebugSomething isn't workingpackage/coreAffects BLite.Coreseverity/mediumSpace leak, inconsistency or failure in a narrow scenario

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions