zeroshade commented on PR #1256:
URL: https://github.com/apache/arrow-go/pull/1256#issuecomment-5511973049

   @fallintoplace confirmed, and thank you — you were right, and the underlying 
problem is broader than either of the two cases you named. Pushed 09060510.
   
   I wrote your `RLE_DICTIONARY -> BYTE_STREAM_SPLIT` repro as a test first. It 
fails on the previous commit exactly as you predicted, at every width:
   
   ```
   --- FAIL: .../width=2/after_RLE_DICTIONARY
       expected: parquet.FixedLenByteArray{0x64, 0x65}
       actual  : parquet.FixedLenByteArray{0x66, 0x67}
       Messages: slot 0 decoded incorrectly
   ```
   
   `dictConverter.Copy` does `o[idx] = dc.dict[val]`, and `Fill` copies one 
header across an RLE run, so repeated indices leave several slots sharing one 
dictionary-backed slice. And as you said, no nulls needed.
   
   While writing it I checked `PLAIN -> BYTE_STREAM_SPLIT` again and found my 
previous commit had only papered over it. `spacedExpandSwap` removes 
*duplicate* headers, but PLAIN's `Decode` does `out[idx] = 
pflba.data[:typeLen]`, so every slot points into the page buffer even with no 
duplicates at all. BSS then overwrites that buffer wholesale:
   
   ```
   --- FAIL: .../width=2/after_PLAIN
       expected: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}
       actual  : []byte{0x64, 0x65, 0x66, 0x67, 0x68, 0x69}
       Messages: decoding wrote through into the PLAIN page buffer
   ```
   
   So the root cause isn't aliasing specifically — it's that a decoder which 
writes *through* caller headers was claiming them on capacity alone, while the 
value buffer is shared across pages and encodings. Capacity can't distinguish 
our storage from a dictionary entry or a page buffer.
   
   Reuse is now gated on provenance: a header is only written through if it 
falls inside the block this decoder allocated; anything else is re-pointed at a 
fresh block.
   
   ### This narrows your GH-1172 contract, so please push back if you disagree
   
   `TestByteStreamSplitFixedLenByteArrayDecoderReusesProvidedOutput` asserted 
that a caller-provided buffer with capacity gets filled in place. That is 
precisely the unsafe behavior, and I could not find a way to keep it — the 
record reader hands us the same `valueBuf` whether the headers are ours or were 
left by the previous page's decoder. I replaced it with two tests: one pinning 
that we do *not* write through caller buffers, one pinning that we still reuse 
our own storage.
   
   Allocation is unchanged where it matters. Preparing only the decoded prefix 
regressed `DecodeSpaced` to 1 alloc/op, because the expansion permutes headers 
across the whole window and unowned ones migrate into the prefix. Preparing the 
whole window once, null slots included, restores it:
   
   | | allocs/op | correct? |
   | --- | --- | --- |
   | before this commit | 0 | **no** |
   | prefix-only preparation | 1 | yes |
   | this commit | **0** | yes |
   
   (4096 slots / 3009 values / width 16, steady state.) The dense `Decode` path 
is untouched — successive windows still allocate one block each, as before, and 
earlier windows keep their values.
   
   `parquet/...` and `arrow/...` pass in full, `-race` is clean on `encoding`, 
`file` and `pqarrow`, and `golangci-lint` reports 0 issues. Both new tests fail 
without the fix.
   
   One thing I'd like your read on: `owns` compares `unsafe.SliceData` pointers 
against the block bounds. It's the honest expression of "did I allocate this", 
and `go vet` is clean, but if you'd rather not have pointer-provenance in this 
path I'm open to alternatives — the only other one I found that is clearly 
correct costs an allocation per page.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to