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]
