Frequent items: empty means zero stream weight, not zero active items - #770
Merged
Merged
Conversation
A purge can remove every counter while the stream weight and offset stay positive. isEmpty() treated that state as empty, so toByteArray() wrote the 8-byte empty form (losing stream weight and offset) and merge() ignored the other sketch. Parallels apache/datasketches-cpp#527 and #529. - isEmpty() returns streamWeight == 0 in FrequentLongsSketch and FrequentItemsSketch. - toByteArray() handles a non-empty sketch with no active items: full preamble, no values or keys. - getInstance(MemorySegment): emptiness is determined by PreLongs; the empty flag is only cross-checked. Reject a non-empty image whose stream weight is not positive. - FrequentLongsSketch string form: flags from isEmpty(); getInstance(String) masks the flag with EMPTY_FLAG_MASK, requires it to agree with the stream weight, and accepts a non-empty sketch with no active items. - PreambleUtil: document emptiness and the flags byte; remove unused SER_DE_ID_SHORT. - Cross-language: generate and check purged-to-zero images; checkCpp() now reads the C++ ascii and utf8 images instead of the Java ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tisonkun
approved these changes
Sep 24, 2026
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.
Java counterpart of apache/datasketches-cpp#527 and apache/datasketches-cpp#529.
Problem
isEmpty()inFrequentLongsSketchandFrequentItemsSketchwasgetNumActiveItems() == 0. A purge subtracts the median from every counter and drops those that reach zero, so with many low-frequency items it can remove every counter whilestreamWeightandoffsetstay positive. That state was reported as empty:toByteArray()wrote the 8-byte empty form, dropping stream weight and offset. After a round trip the sketch looked fresh and exact.merge()returned early and ignored the other sketch's stream weight and offset.Reproducer:
new FrequentLongsSketch(256)updated with 193 distinct items. Before this change it reportsisEmpty() == truewith stream length 193 and error 1; after a byte round trip, stream length 0 and error 0; merged into a sketch with one item, stream length 1 and error 0.A non-empty sketch with no items is not equivalent to a fresh one: it says every item's count is at most
offset, and its stream length counts.Changes
isEmpty()returnsstreamWeight == 0in both classes. Updates only accept positive counts and merges only add, so onlyreset()returns a sketch to empty.toByteArray()handles zero active items in a non-empty sketch: full preamble withactiveItems = 0and no values or keys (32 bytes). This path was unreachable before, and the hash maps returnnullactive arrays when empty.FrequentLongsSketchstring form:serializeToString()sets the flag fromisEmpty();getInstance(String)masks the flag withEMPTY_FLAG_MASK(wasflags > 0), requires it to agree with the stream weight, and no longer rejects a non-empty sketch with no active items.PreambleUtil: documented emptiness and the flags byte (both legacy bits written, either accepted, no other bits defined), fixed the row label of the data start in the layout diagram, and removed the unusedSER_DE_ID_SHORT.No change to the binary format. Every image written by an earlier version still deserializes; purged-to-zero images written earlier were already stored as empty, and their stream weight and offset cannot be recovered.
Tests
reset()after a purge returns to the empty 8-byte form.LongsSketchTest.checkStringDeserEmptyCorruptasserted the old rule (its input is a valid purged-to-zero sketch); it is nowcheckStringDeserNonEmptyNoItems.Cross-language
FrequentItemsSketchCrossLanguageTestgeneratesfrequent_long_purged_java.skandfrequent_string_purged_java.sk(lgMaxMapSize = 8, 193 distinct items) and checks them for Java, C++ and Go. The C++ images come from frequent_items_sketch: add reset(), decide emptiness by preamble longs datasketches-cpp#529; the Java and C++ images are byte-identical for both longs and strings.checkCpp()calledstringsAsciiandstringsUtf8withGroupLanguage.JAVA, so it never read the C++ ascii and utf8 images. It now usesGroupLanguage.CPP.Go reads these images too;
checkGo()will expose the same bug in datasketches-go once it generates the purged images.Unrelated failures
BloomFilterTest.basicDifferenceTestandTDigestDoubleSerializationTest.weightOverflowDoesNotChangeDigestfail onmainwithout this change.🤖 Generated with Claude Code