Skip to content

feat(fsst): rewire writer/reader adapters onto the fsst module#295

Merged
dfa1 merged 1 commit into
mainfrom
feat/fsst-rewire-adapters
Jul 20, 2026
Merged

feat(fsst): rewire writer/reader adapters onto the fsst module#295
dfa1 merged 1 commit into
mainfrom
feat/fsst-rewire-adapters

Conversation

@dfa1

@dfa1 dfa1 commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

PR 5 of #287 — the only PR touching existing production code. Reduces writer/FsstEncodingEncoder and reader/FsstEncodingDecoder to thin wire adapters over the standalone fsst module (PRs 1-4, already merged). Wire format is unchanged; no test files modified (that's PR 6).

  • Deleted (writer): the ~250-line inline SymbolTable/SymbolCandidate machinery — training, stratified sampling, the open-addressing match index. All of that now lives in CompressorBuilder/Compressor/Matcher.
  • Deleted (reader): the byte-by-byte decompressString loop, replaced by Decompressor's unconditional-8-byte-store decode, with the documented +7 slack allocation and a read-only slice back to the true logical length so callers still see an exactly-sized buffer.
  • Kept: all wire/metadata plumbing — UTF-8 conversion, ProtoFSSTMetadata + narrowestUnsigned ptype selection, the two-child EncodeNode/EncodeResult tree, arena allocation, readUnsigned dispatch, VarBinArray.OffsetMode. TRAINING_SAMPLE_SEED now feeds CompressorBuilder.seed().
  • Wire-order code remapping (the subtle part): the compressor numbers codes gain-descending internally, but the wire format requires length-sorted order. The commit message explains the wireOrder/internalToWire inverse-permutation logic in full; I reviewed it independently and the arithmetic on both the symbol-table population and the code-stream remapping checks out.

Two real bugs were caught and fixed earlier in this sequence (PR 3's boundary-overrun match, since carried through PR 4) — none new found in this PR; I independently re-verified the interop gates myself rather than just trusting the agent's report, given this PR's risk level.

Test plan

  • ./mvnw test -pl writer,reader -am — 1713 tests, 0 failures, zero test-file changes
  • ./mvnw verify -pl writer,reader -am — checkstyle/javadoc clean
  • JavaWritesRustReadsIntegrationTest (full class, 217 tests incl. javaWriter_jniReader_fsstUtf8Column) — the real Rust reference reader decoding this encoder's output
  • RustJavaReaderComparisonIntegrationTest (fsst.vortex fixture — our decoder reading a Rust-written file) and FileSizeComparisonIntegrationTest#highCardinalityUtf8_javaVsJni (FSST still dispatch-selected) — 251 tests total across all three classes, 0 failures, 2 unrelated skips
  • ./mvnw verify -DskipTests at root — full reactor SUCCESS
  • ./mvnw javadoc:javadoc -pl writer,reader — zero new diagnostics

🤖 Generated with Claude Code

PR 5 of #287: reduce the two FSST production files to thin wire adapters
over the standalone vortex-fsst module. Only production code touched; no
test files changed (that is PR 6's scope). The wire format is unchanged.

Deleted (writer): the ~250-line inline SymbolTable/SymbolCandidate machinery
— training loop (trainSymbolTable), stratified sampling (sampleRowIndices),
the open-addressing longest-match index, candidate counting, and per-row
compress. All of that now lives in CompressorBuilder/Compressor/Matcher.

Deleted (reader): the byte-by-byte decompressString loop. Decoding now goes
through Decompressor (the unconditional 8-byte-store Algorithm 1).

Kept (both): all wire/metadata plumbing — UTF-8 conversion, ProtoFSSTMetadata
with narrowestUnsigned ptype selection, the EncodeNode/EncodeResult two-child
tree (uncompLens/codesOff), arena allocation of every output buffer, the
readUnsigned ptype dispatch, and the VarBinArray.OffsetMode result. The
TRAINING_SAMPLE_SEED reproducibility constant is retained and now feeds
CompressorBuilder.seed().

Wire-order code remapping (the subtle part — over-explained on purpose):
The Compressor numbers its codes in gain-descending order, but the vortex.fsst
wire format lays symbols out in length order (all multi-byte length-ascending,
then length-1 last; mirrors Rust FSSTData::validate_symbol_lengths). So the
writer must express BOTH the symbol table AND the code stream in wire codes.

  wireOrder = compressor.codesSortedByLength() — wireOrder[i] is the internal
    (gain-descending) code that belongs at wire position i.
  internalToWire[wireOrder[i]] = i — the inverse permutation, mapping every
    internal code the compressor emits to its wire position.

Symbol table buffers: at wire position i write packedSymbol(wireOrder[i]) and
symbolLength(wireOrder[i]) — the symbol living at wire slot i is internal
symbol wireOrder[i]. Code stream: compressor.compress emits internal codes;
each non-escape code byte c is rewritten to internalToWire[c]. The escape byte
0xFF and the single literal byte following it are copied through untouched —
the literal is raw data, not a code, and 0xFF never collides with a code since
codes are 0..254 (numSymbols <= 255). Reader side: it reads the wire symbol
table into code-indexed arrays and a code b in the stream indexes wire slot b
directly, so encoder position i and decoder index i refer to the same symbol —
the file is self-consistent and round-trips.

Verified: (1) writer,reader unit suite green with zero test changes (1713
tests); (2) checkstyle/javadoc gate green; (3) JavaWritesRustReadsIntegration
javaWriter_jniReader_fsstUtf8Column — the real Rust reference reader decodes
this encoder's output; (4) RustJavaReaderComparison fsst.vortex fixture (our
decoder reads a Rust-written FSST file) and FileSizeComparison
highCardinalityUtf8_javaVsJni (FSST still dispatch-selected) green; (5) full
reactor build green; plus a throwaway 11-row driver proving encode→remap→wire
symbol table→Decompressor reproduces every input byte-for-byte with the wire
table in valid length order.

Also fixed the empty-table read: with 0 trained symbols the writer floors the
symbol buffers to a 1-byte placeholder, so the decoder derives the symbol count
from symbolsBuf.byteSize()/8 (floors to 0) rather than symbolLensBuf.byteSize().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dfa1
dfa1 merged commit 1b9714c into main Jul 20, 2026
6 checks passed
@dfa1 dfa1 mentioned this pull request Jul 20, 2026
@dfa1
dfa1 deleted the feat/fsst-rewire-adapters branch July 20, 2026 20:58
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