Skip to content

FSST: true per-row lazy/random-access decode - #392

Merged
dfa1 merged 1 commit into
mainfrom
fsst-per-row-lazy-decode
Sep 13, 2026
Merged

dfa1 merged 1 commit into
mainfrom
fsst-per-row-lazy-decode

Conversation

@dfa1

@dfa1 dfa1 commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • FsstEncodingDecoder.decode() no longer decompresses the whole column eagerly — it returns a new LazyFsstVarBinArray that decompresses row i's code range only when row i is actually read.
  • getByteLength/forEachByteLength answer straight from the uncompressed-lengths child, with no decompression at all — the exact waste this closes for filter/projection/length-only workloads.
  • Per-row validation (malformed lengths/code offsets) moves from decode-time to access-time, mirroring the existing VarBinArrays.checkedLength "don't scan untrusted offsets eagerly" convention, and is strictly more precise than the old aggregate check.
  • New ADR 0026 documents this as a narrowing of ADR 0010's "decompression encodings stay eager" exclusion — FSST's per-row code range is independent of its neighbors, unlike Bitpacked/Pco/Zstd.

Test plan

  • ./mvnw -pl reader -am verify (javadoc + unit tests)
  • FsstEncodingDecoderTest guards updated to trigger via row accessors; new Laziness nested class proves decode() succeeds on a column with one corrupt row until that row is read, and that forEachByteLength never touches the code-offsets/compressed buffers
  • Integration: RustWritesJavaReadsIntegrationTest, RustJavaReaderComparisonIntegrationTest, DictValuesPoolCompressionIntegrationTest (FSST nested under vortex.dict) all pass unchanged against real Rust-written files

🤖 Generated with Claude Code

https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi

FsstEncodingDecoder.decode() no longer decompresses the whole column
eagerly. FSST's per-row code range is independent of every other row
(unlike Bitpacked/Pco/Zstd, which need a decoded window), so it returns
a LazyFsstVarBinArray that decompresses row i only when row i is
actually read; getByteLength/forEachByteLength answer straight from the
uncompressed-lengths child with no decompression at all. See ADR 0026.

Per-row bounds validation (malformed lengths/code offsets) moves from
decode-time to access-time, mirroring VarBinArrays' existing "offsets
are not scanned at decode time" convention, and is strictly more
precise than the old aggregate check it replaces.

Adversarial edge-case tests against the new accessors found two raw
JDK exceptions escaping getBytes() in violation of the security
contract — a truncated escape code with no literal byte after it, and
a code byte naming an untrained symbol index — both now caught around
the decompress() call and rewrapped as VortexException. Also adds
coverage for zero rows, an empty row amid non-empty ones, independent
access to a later row when an earlier row is structurally invalid,
idempotent repeated access, the exact claimed-length-equals-bound case,
a negative code offset, and the Integer.MAX_VALUE-7 code-range overflow
guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi
@dfa1
dfa1 force-pushed the fsst-per-row-lazy-decode branch from d369a69 to fcfe48f Compare September 13, 2026 18:20
@dfa1
dfa1 merged commit f694fe7 into main Sep 13, 2026
6 checks passed
@dfa1
dfa1 deleted the fsst-per-row-lazy-decode branch September 13, 2026 18:31
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