Skip to content

refactor(writer): centralize zone-map MIN/MAX stats computation (ADR 0025) - #391

Merged
dfa1 merged 5 commits into
mainfrom
generalize/central-zone-map-stats
Sep 13, 2026
Merged

dfa1 merged 5 commits into
mainfrom
generalize/central-zone-map-stats

Conversation

@dfa1

@dfa1 dfa1 commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #382/#384-#387: instead of relying on every EncodingEncoder to remember to compute its own zone-map stats (the exact convention thirteen encoders independently forgot), move the required computation to the one choke point every written segment already passes through -- VortexWriter#writeSegment.

  • New ZoneMapStatCodec#columnMinMax(dtype, data) -- a generic fallback computed from the segment's original, untouched input, mirroring columnSum (which already worked this way and never had this bug).
  • writeSegment uses the winning encoder's own stats when present, falls back to columnMinMax otherwise. Encoders may still report a cheaper override; none are required to.
  • Kept as overrides (genuine efficiency wins): Constant/Sequence (O(1), already known from validation) and RunEnd/ZigZag (folded into a pass they run anyway).
  • Removed as redundant (a second, unfused pass with no benefit over the one fallback pass): Pco, Rle, Sparse, Patched, Zstd, Fsst, VarBinView, Ext's cascade path, DateTimeParts.
  • New ComparableValues marker interface (implemented by DateTimePartsData) for carriers whose logical comparable value isn't their literal shape -- validated against the Rust reference, which has no DateTimeParts-specific stats code either: it decodes to canonical form and runs the same generic reduction. Java's writer doesn't need to decode to get there; the carrier already has the pre-split values.
  • ZoneMapStatsCoverageTest (asserting every encoder self-reports) is replaced by ZoneMapStatCodecTest (asserting columnMinMax handles every dtype shape) -- the actual guarantee moved from a per-encoder enumeration to the new choke point itself.

Full design rationale, alternatives considered, and the Rust cross-check: ADR 0025.

Test plan

  • ./mvnw verify green across the whole reactor
  • New ZoneMapStatCodecTest: every dtype shape (Primitive signed/unsigned, Extension+Primitive storage, Extension+ComparableValues, Utf8, each nullable) plus excluded shapes (Decimal, Bool, Binary, structural)
  • New SequenceEncodingEncoderTest.Stats: direct coverage for the kept override now that the fitness test moved
  • All existing per-encoder round-trip tests still pass (no behavior change, only where the stats now come from)

🤖 Generated with Claude Code

https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi

…0025)

Thirteen encoders across #382/#384-#387 independently forgot to
compute zone-map stats -- not one bad encoder, an architecture with
no structural guarantee, closed only by a coverage test that itself
has to be remembered for every future encoder.

VortexWriter#writeSegment (the one choke point every written segment
already passes through) now falls back to a new
ZoneMapStatCodec#columnMinMax whenever the winning encoder's own
EncodeResult/CascadeStep didn't already supply stats, computed
directly from the segment's untouched (dtype, data) -- mirroring
columnSum, which already worked this way and never had this bug.
Encoders may still report a cheaper override (kept for
Constant/Sequence/RunEnd/ZigZag); none are required to anymore.

Confirmed against the Rust reference (spiraldb/vortex): it has no
per-encoding stats special-casing either -- StatsSet::compute_stat
runs one generic min_max reduction over every array's canonical form.
The new ComparableValues marker interface (implemented by
DateTimePartsData) is the write-side mirror of exactly what Rust's
own canonical.rs::decode_to_temporal does for the same encoding: no
special-casing in the generic path, just a carrier that knows its own
comparable form.

ZoneMapStatsCoverageTest (asserting every encoder self-reports) is
replaced by ZoneMapStatCodecTest (asserting columnMinMax handles
every dtype shape) -- the guarantee moved from a per-encoder
enumeration to the actual new choke point.
Pco, Rle, Sparse, Patched, Zstd, Fsst, VarBinView, Ext's cascade
path, and DateTimeParts each computed zone-map stats as a second,
unfused pass over the same input the generic fallback
(ZoneMapStatCodec#columnMinMax, ADR 0025) now covers from
VortexWriter#writeSegment -- same cost either way, but duplicated
nine times instead of once. Net code reduction, not a new
abstraction on top of the old one.
Sequence keeps its O(1) stats override (an arithmetic sequence's
extremes are always its first/last element) since ZoneMapStatsCoverageTest,
which used to cover this indirectly, was replaced by ZoneMapStatCodecTest
in the prior commit -- the four kept overrides (Constant/Sequence/
RunEnd/ZigZag) need their own direct coverage now that the fitness
test moved to testing the fallback instead of every encoder.
The same one-liner restated in nine near-identical files is noise,
not documentation -- the why (ADR 0025, the commit history) already
explains it once, in one place.
Added earlier in this same change to de-duplicate the "stats != null
? stats[0] : null" pattern across nine encoders -- then all nine had
their stats computation removed outright once it moved to the
central fallback, leaving both factories with zero call sites. None
of the four kept overrides (Constant/Sequence/RunEnd/ZigZag) produce
a {min, max} pair either, so they never fit here anyway.
@dfa1
dfa1 merged commit a2db956 into main Sep 13, 2026
6 checks passed
@dfa1
dfa1 deleted the generalize/central-zone-map-stats branch September 13, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant