This is an automated email from the ASF dual-hosted git repository.

freakyzoidberg pushed a commit to branch fix-theta-empty-seed-hash
in repository https://gitbox.apache.org/repos/asf/datasketches-go.git

commit b6d8ffdf070f4560e34cce98399a43258bc8cdba
Author: Pierre Lacave <[email protected]>
AuthorDate: Tue Sep 29 12:50:10 2026 +0200

    fix(theta): do not store the seed hash of an empty compact sketch
    
    C++ and Java leave the seed hash bytes (6, 7) at zero when serializing
    an empty compact sketch, but Go wrote the seed hash, so theta_n0 was
    the only theta image that differed between the languages (#186).
    Readers already ignore these bytes for an empty image, so existing
    images remain readable.
    
    An empty sketch decoded from such an image reports a zero seed hash, so
    A-not-B now skips the seed hash check for an empty B, as C++ does.
    
    Regenerate theta_n0_go.sk.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../go_generated_files/theta_n0_go.sk              | Bin 8 -> 8 bytes
 theta/a_not_b.go                                   |   3 ++-
 theta/a_not_b_test.go                              |  22 +++++++++++++++++++++
 theta/encoder.go                                   |   8 +++++---
 theta/sketch_serialization_test.go                 |  18 +++++++++++++++++
 5 files changed, 47 insertions(+), 4 deletions(-)

diff --git a/serialization_test_data/go_generated_files/theta_n0_go.sk 
b/serialization_test_data/go_generated_files/theta_n0_go.sk
index 44730d3..f6c647f 100644
Binary files a/serialization_test_data/go_generated_files/theta_n0_go.sk and 
b/serialization_test_data/go_generated_files/theta_n0_go.sk differ
diff --git a/theta/a_not_b.go b/theta/a_not_b.go
index 1224f96..cc832e5 100644
--- a/theta/a_not_b.go
+++ b/theta/a_not_b.go
@@ -49,7 +49,8 @@ func ANotB(a, b Sketch, seed uint64, ordered bool) 
(*CompactSketch, error) {
        if aSeedHash != uint16(seedHash) {
                return nil, fmt.Errorf("sketch A seed hash mismatch: expected 
%d, got %d", seedHash, aSeedHash)
        }
-       if bSeedHash != uint16(seedHash) {
+       // an empty sketch does not carry a seed hash in its serialized form
+       if !b.IsEmpty() && bSeedHash != uint16(seedHash) {
                return nil, fmt.Errorf("sketch B seed hash mismatch: expected 
%d, got %d", seedHash, bSeedHash)
        }
 
diff --git a/theta/a_not_b_test.go b/theta/a_not_b_test.go
index b861415..8e9c776 100644
--- a/theta/a_not_b_test.go
+++ b/theta/a_not_b_test.go
@@ -184,6 +184,28 @@ func TestANotB(t *testing.T) {
                assert.False(t, result.IsEmpty())
        })
 
+       t.Run("A Non Empty No Retained Keys, B Empty Decoded", func(t 
*testing.T) {
+               a, err := NewQuickSelectUpdateSketch(WithUpdateSketchLgK(5), 
WithUpdateSketchP(0.1))
+               assert.NoError(t, err)
+               err = a.UpdateInt64(6)
+               assertUpdate(t, err)
+
+               b, err := NewQuickSelectUpdateSketch(WithUpdateSketchLgK(5))
+               assert.NoError(t, err)
+               // the serialized image of an empty sketch has a zero seed hash
+               data, err := b.CompactOrdered().MarshalBinary()
+               assert.NoError(t, err)
+               decodedB, err := Decode(data, DefaultSeed)
+               assert.NoError(t, err)
+
+               result, err := ANotB(a, decodedB, DefaultSeed, true)
+               assert.NoError(t, err)
+
+               assert.InDelta(t, 0.1, result.Theta(), 1e-8)
+               assert.Equal(t, uint32(0), result.NumRetained())
+               assert.False(t, result.IsEmpty())
+       })
+
        t.Run("A Empty, B Estimation Mode", func(t *testing.T) {
                a, err := NewQuickSelectUpdateSketch(WithUpdateSketchLgK(5))
                assert.NoError(t, err)
diff --git a/theta/encoder.go b/theta/encoder.go
index a1de24a..9e72a4f 100644
--- a/theta/encoder.go
+++ b/theta/encoder.go
@@ -181,9 +181,11 @@ func (enc *Encoder) encodeSketch(sketch *CompactSketch, 
bytes []byte, offset int
        bytes[offset] = flags
        offset++
 
-       // Seed hash
-       seedHash, _ := sketch.SeedHash()
-       binary.LittleEndian.PutUint16(bytes[offset:offset+2], seedHash)
+       // Seed hash, left as zero for an empty sketch as in C++ and Java
+       if !sketch.IsEmpty() {
+               seedHash, _ := sketch.SeedHash()
+               binary.LittleEndian.PutUint16(bytes[offset:offset+2], seedHash)
+       }
        offset += 2
 
        if preambleLongs > 1 {
diff --git a/theta/sketch_serialization_test.go 
b/theta/sketch_serialization_test.go
index 97fee13..5e99c12 100644
--- a/theta/sketch_serialization_test.go
+++ b/theta/sketch_serialization_test.go
@@ -697,6 +697,24 @@ func TestEncodingAndDecoding(t *testing.T) {
                assert.Equal(t, uncompressedBuf.Len(), compressedBuf.Len())
        })
 
+       t.Run("Empty sketch without seed hash", func(t *testing.T) {
+               sketch, _ := NewQuickSelectUpdateSketch()
+               compact := sketch.CompactOrdered()
+
+               data, err := compact.MarshalBinary()
+               assert.NoError(t, err)
+
+               // same image as C++ and Java: preLongs, serVer, family, 2 
unused, flags
+               // (read only, empty, compact, ordered) and a zero seed hash
+               assert.Equal(t, []byte{1, 3, 3, 0, 0, 0x1e, 0, 0}, data)
+
+               // the seed hash of an empty sketch is not checked on read
+               decoded, err := Decode(data, 9999)
+               assert.NoError(t, err)
+               assert.True(t, decoded.IsEmpty())
+               assert.Equal(t, uint32(0), decoded.NumRetained())
+       })
+
        t.Run("Compressed sketch with custom seed", func(t *testing.T) {
                customSeed := uint64(9999)
                sketch, _ := 
NewQuickSelectUpdateSketch(WithUpdateSketchSeed(customSeed))


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to