Skip to content

fix(reader): make constant Utf8/Binary decode lazy - #331

Merged
dfa1 merged 1 commit into
mainfrom
fix/lazy-constant-varbin
Aug 6, 2026
Merged

fix(reader): make constant Utf8/Binary decode lazy#331
dfa1 merged 1 commit into
mainfrom
fix/lazy-constant-varbin

Conversation

@dfa1

@dfa1 dfa1 commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Closes ConstantEncodingDecoder.decodeString should be lazy, not eager #329.
  • ConstantEncodingDecoder.decodeString was the one vortex.constant value type without a lazy broadcast array: every other type (primitive, bool, decimal) returns a single stored value per row in O(1) via a LazyConstantXxxArray; strings eagerly wrote n copies of the scalar into a real OffsetMode buffer (n * strLen allocation + copy) — the one crash-adjacent gap left after the RunEnd/Constant/Zoned/Pco adversarial-input hardening batch (PR fix(reader): harden RunEnd/Constant/Zoned/Pco against malformed input #330).
  • Added VarBinArray.ConstantMode: holds the scalar's bytes once, every accessor returns it for any row, no buffer allocated. Follows the same "no single contiguous segment" convention ChunkedMode/ViewMode already use, so generic consumers (e.g. VarBinArray.toOffsetMode) still flatten it correctly when they need a real buffer.
  • decodeString now builds ConstantMode directly instead of the eager OffsetMode.
  • docs/compatibility.md updated to credit VarBinArray.ConstantMode alongside LazyConstantXxxArray for vortex.constant.

Test plan

  • ./mvnw -pl fsst,core,reader -am verify -DskipITs
  • ./mvnw -pl writer,integration -am verify (real-world files with constant Utf8/Binary columns)
  • ./mvnw -pl integration -am verify -Dit.test=DocsConsistencyTest -Dvortex.it.excludedGroups=
  • New tests: VarBinArrayTest.Constant (accessor/copy/limited/bounds/toOffsetMode contracts) and ConstantEncodingDecoderTest (decodes to ConstantMode; a row count too large to ever eagerly materialize still decodes instantly)

🤖 Generated with Claude Code

ConstantEncodingDecoder.decodeString was the one vortex.constant value
type without a LazyConstantXxxArray: every other type (primitive, bool,
decimal) broadcasts a single stored value per row in O(1), but strings
eagerly wrote n copies of the scalar into a real OffsetMode buffer
(n * strLen allocation + copy), the only crash-adjacent risk that
survived the RunEnd/Constant/Zoned/Pco hardening batch (a large n * strLen
product could overflow negative or just OOM).

Root cause: VarBinArray's only flat representation, OffsetMode, has no
broadcast/modulo path the way AbstractMaterializedArray gives the
primitive Materialized*Array types. Added VarBinArray.ConstantMode: a
sealed-permitted record holding the scalar's bytes once, with every
accessor returning it for any row — bytesSegment()/segmentIfPresent()
follow the same "no single contiguous buffer" convention already used by
ChunkedMode/ViewMode, so generic consumers still flatten it correctly via
VarBinArray.toOffsetMode(). decodeString now builds this directly instead
of an eager OffsetMode.

docs/compatibility.md: vortex.constant's Notes column now credits
VarBinArray.ConstantMode alongside LazyConstantXxxArray.
@dfa1
dfa1 merged commit 987fe41 into main Aug 6, 2026
6 checks passed
@dfa1
dfa1 deleted the fix/lazy-constant-varbin branch August 6, 2026 07:02
dfa1 added a commit that referenced this pull request Aug 6, 2026
VarBinArray was the one Array sub-hierarchy still declared sealed with
an explicit permits list, unlike its siblings (ByteArray, LongArray,
DecimalArray, ...), which reopen the hierarchy via non-sealed and let
each representation live as an ordinary top-level class. Nothing in the
codebase does an exhaustive switch over VarBinArray's specific modes
(grepped: zero matches), so the sealing bought no compiler-enforced
exhaustiveness — only coupling the interface's declaration to the full
list of its implementations, and forcing every new representation (most
recently VarBinConstantArray, #331) to be nested inside VarBinArray.java
and added to its permits clause.

VarBinArray is now `non-sealed`. Each former nested record is a
top-level class in reader.array, renamed to match the sibling families'
self-describing naming (MaterializedByteArray, LazyConstantLongArray, ...):

- OffsetMode   -> VarBinOffsetArray
- DictMode     -> VarBinDictArray
- ChunkedMode  -> VarBinChunkedArray
- ViewMode     -> VarBinViewArray
- SlicedMode   -> VarBinSlicedArray
- ConstantMode -> VarBinConstantArray

checkedLength (bounds-check shared by the offset- and dict-backed
representations) moved to a new package-private VarBinArrays helper,
mirroring the RunEndArrays precedent, since it can no longer be a
private interface method reachable from sibling top-level classes.

Pure rename/move: no behavior change. All call sites across
reader/writer/cli updated; docs/compatibility.md's Notes column
updated to the new class names.
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.

ConstantEncodingDecoder.decodeString should be lazy, not eager

1 participant