Skip to content

fix: read string-dict offsets at their true ptype width (#215)#218

Merged
dfa1 merged 1 commit into
mainfrom
fix/215-dict-offsets
Jul 6, 2026
Merged

fix: read string-dict offsets at their true ptype width (#215)#218
dfa1 merged 1 commit into
mainfrom
fix/215-dict-offsets

Conversation

@dfa1

@dfa1 dfa1 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Fixes #215, found by Raincloud triage round 2 (#205) on uci-magic-gamma-telescope.

Root cause — the decoder carrier, not the buffer: DictEncodingDecoder.decodeUtf8DictProto passed a hardcoded PType.I64 offsets ptype to VarBinArray.ofDict, but toOffsetMode's fast path returns the decoded array unchanged with its own ptype — FSST decompresses to I32 offsets (12-byte buffer, 4-byte stride). DictMode.dictReadOff then did 8-byte reads over a 4-byte-stride buffer → raw IndexOutOfBoundsException. Per the Rust reference (encodings/fsst/src/array.rs + canonical.rs), the narrow codes_offsets wire ptype is consumed only during decompression; the canonicalized value offsets are authoritative.

Fix (both remedies): the carrier now uses dictValues.offsetsPtype() so ptype and buffer stride agree, and dictReadOff reads every signed/unsigned offset width at its true stride with a VortexException bounds guard replacing the bare IOOBE (ADR 0003 discipline) — this also retires the narrowly-scoped IOOBE arm in the conformance harness's gap catch.

Validation: VarBinArrayTest Dict group 3 → 12 tests (all 8 offset ptypes parameterized + the exact #215 mislabeled-width guard); reader suite 1113 green; magic-gamma exports all 19,020 rows and matches the Parquet oracle on all 209,220 cells — matrix entry flipped to ok (19 ok / 1 gap / 227 untriaged).

Closes #215

🤖 Generated with Claude Code

A `vortex.dict` string column whose values are FSST-compressed crashed the
scan with a raw `IndexOutOfBoundsException` in `VarBinArray$DictMode.dictReadOff`.

Root cause: `DictEncodingDecoder.decodeUtf8DictProto` ran the decoded values
through `VarBinArray.toOffsetMode` and then hardcoded `PType.I64` as the
offsets ptype of the resulting `DictMode`. But `toOffsetMode` only builds fresh
I64 offsets on its slow path — on the fast path it returns the already-decoded
`OffsetMode` unchanged, keeping its own ptype. FSST decompresses its values to
a child with I32 offsets (Rust: FSST's `codes_offsets_ptype` is a narrow wire
ptype read during decompression — see encodings/fsst/src/array.rs — while the
canonicalized value offsets are freshly built and independent of it). The
DictMode carrier thus claimed an 8-byte stride over a 4-byte-stride buffer
(12 bytes for 3 offsets), so reading offset 1 as an 8-byte long walked off the
segment.

Fix: carry `dictValues.offsetsPtype()` so the carrier matches the buffer it
actually materialized (parse-don't-validate). Additionally harden `dictReadOff`
to read every signed/unsigned offset width at its true stride and to throw a
helpful `VortexException` on out-of-range indices rather than a bare
`IndexOutOfBoundsException` (ADR 0003 bounds discipline).

Corpus evidence: uci-magic-gamma-telescope (Raincloud, vortex-data 0.69.0) — its
utf8 `class` column is a `vortex.dict` over `vortex.fsst` values. Export now
completes and matches the Parquet sibling on all 19020 rows x 11 cols.

Closes #215

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dfa1 dfa1 merged commit 1636234 into main Jul 6, 2026
6 checks passed
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.

VarBinArray.DictMode.dictReadOff: U8/U16 offset ptypes fall through to 8-byte reads — IOOBE on dict-over-fsst strings

1 participant