fix(parquet): stop BYTE_STREAM_SPLIT FLBA spaced decode aliasing output - #1256
fix(parquet): stop BYTE_STREAM_SPLIT FLBA spaced decode aliasing output#1256zeroshade wants to merge 1 commit into
Conversation
310788a to
c30fbda
Compare
|
Checked how this composes with #1172, since both touch They merge cleanly with no conflict ( Allocation behaviour for
So the two are complementary rather than competing: #1172's contiguous cold-output block absorbs the entries that need storage, and |
A BYTE_STREAM_SPLIT FIXED_LEN_BYTE_ARRAY column containing nulls and spanning more than one data page decoded values shifted by one position, silently and without an error. spacedExpand moves values into their spaced positions with copy and deliberately leaves the null slots alone. For the scalar column types that is fine, but ByteArray and FixedLenByteArray buffers hold slice headers, so copying leaves duplicate headers behind: a null slot and a valid slot end up referencing the same backing array. That is still harmless for a decoder that replaces the header, which is what PlainFixedLenByteArrayDecoder does. ByteStreamSplitFixedLenByteArrayDecoder instead writes through the caller's existing slice, so once the record reader reused its value buffer for the next page two output slots shared one backing array and clobbered each other. Add spacedExpandSwap, which swaps rather than copies so the buffer stays a permutation of its original elements, and use it from the byte-stream-split decoder. No slot aliases another and every slot keeps its reusable capacity, so the buffer reuse the decoder relies on is preserved. spacedExpand itself is untouched, leaving every other column type and the non-spaced path unaffected. Fixes apache#1255
c30fbda to
89c8b6d
Compare
|
Rebased onto current #1220 added an early return to idxDecode -= run.Length
// Once the decoded prefix is already aligned every remaining swap is a
// self-swap, so there is nothing left to do. Mirrors spacedExpand.
if idxDecode == run.Pos {
return numValues
}Re-verified on the new base: the randomized differential test now compares against the early-returning The earlier measurement stands: with #1172 now on |
|
I would request changes here because I think there is still one case this doesn't handle. I tried After the PLAIN page, So something like So I think one value can already be overwritten before Maybe worth adding a |
Rationale for this change
Fixes #1255.
A
BYTE_STREAM_SPLITFIXED_LEN_BYTE_ARRAYcolumn that contains nulls and spans more than one data page decodes values shifted by one position — silently, with no error returned.spacedExpandmoves values into their spaced positions withcopyand deliberately does not clean up the null slots:That reasoning holds for the scalar column types, whose buffers hold values. But
ByteArray/FixedLenByteArraybuffers hold slice headers, socopyleaves duplicate headers behind: after expansion a null slot and a valid slot can reference the same backing array.That is still harmless for a decoder that replaces the header, which is what
PlainFixedLenByteArrayDecoderdoes:ByteStreamSplitFixedLenByteArrayDecoderinstead writes through the caller's existing slice:So once
flbaRecordReaderreused its value buffer for the next page, two output slots shared one backing array and clobbered each other. This explains the full shape of the bug: BYTE_STREAM_SPLIT only, nulls required (to create the duplicates), and two or more pages required (the first page creates the aliases, the second decodes into them).What changes are included in this PR?
spacedExpandSwap, which swaps instead of copying so the buffer remains a permutation of its original elements — no slot aliases another, and every slot keeps its reusable capacity.ByteStreamSplitFixedLenByteArrayDecoder.DecodeSpaced.spacedExpanditself untouched, so every other column type and the non-spaced path are unaffected.spacedExpandSwapplaces values in exactly the same slots asspacedExpandwhile never leaving duplicates.pqarrowround-trip test over a multi-page, nullable BSS FLBA column — the integration-level case that was returning wrong data.The decoder-level regression test fails at every width without the fix.
Why swap rather than the simpler alternatives?
I measured two other approaches and rejected both:
Making
spacedExpanditself swap is correct but replacesmemmovewith element-wise swaps for every column type, which is far too expensive on sparse-null runs:clear(out[:toRead])inDecodeSpacedis a one-liner, but discards all reusable headers. On 4096 slots / 3511 values / width 16 that is 3511 allocations perDecodeSpacedonmaintoday (it would drop to 1 once #1172 lands, but the fix should not depend on that).Swapping keeps the reuse. Cost on the affected path only (
BenchmarkBSSFLBADecodeSpaced, 8192 slots):Steady-state allocations stay at 1–2 per call. A ~25% geomean cost on a path that is currently returning incorrect data seemed clearly worth it, and nothing outside BSS FLBA spaced decoding is touched.
Are these changes tested?
Yes — new tests described above.
parquet/...passes in full withPARQUET_TEST_DATAsupplied, and-raceis clean onparquet/internal/encoding.Are there any user-facing changes?
Yes: BYTE_STREAM_SPLIT FIXED_LEN_BYTE_ARRAY columns with nulls spanning multiple data pages now decode correctly. Previously affected reads returned silently incorrect values.
This PR contains a "Critical Fix". Reading an affected file produced wrong values with no error, which could have been persisted or acted on downstream without any indication of a problem.