This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 4c374d65 fix(parquet/internal/encoding): reset delta bit pack encoder 
state after flush (#1400)
4c374d65 is described below

commit 4c374d650b9a02fe2b0690f60ec5a4c1f692670a
Author: Matt Topol <[email protected]>
AuthorDate: Wed Oct 21 21:11:27 2026 +0100

    fix(parquet/internal/encoding): reset delta bit pack encoder state after 
flush (#1400)
    
    ### Rationale for this change
    
    `deltaBitPackEncoder.FlushValues` hands the sink's bytes to the caller
    but left the bit writer (and the first/current value) in place. The next
    `Put` replaces the writer, but a page whose values are all null calls
    `Put` with nothing, so its flush still used the old writer's byte count
    and copied that many bytes out of the fresh sink buffer:
    
    - **Through a column writer** (pqarrow): no crash and the data reads
    back correctly, but an all-null page is padded with the previous page's
    length in zeros, and the stale `EstimatedDataEncodedSize` makes the
    writer cut pages early. A nullable INT32 column of 20,000 random values,
    5,000 nulls and 10 values, written with 1 KB pages, came out at 92,002
    bytes / 25 pages on main versus 82,954 bytes / 21 pages with this
    change.
    - **Using the encoder directly**: after a large page, the next all-null
    flush panics (`slice bounds out of range [:96061] with capacity 2048`).
    
    Found while reviewing #1396.
    
    ### What changes are included in this PR?
    
    `FlushValues` drops the bit writer and resets the first/current value
    after flushing, so an empty page is exactly the header a fresh encoder
    writes and the size estimate is 0 until the next `Put`. No API change.
    
    ### Are these changes tested?
    
    Yes. `TestDeltaBitPackedReuseAfterAllNullPage` (in `encoding_test.go`,
    next to the other delta tests) writes a first page of 3, 1,000 or
    100,000 values and then checks that an all-null page is byte-identical
    to a fresh encoder's, that the size estimate is 0, and that the
    following page still decodes. It fails on main (wrong bytes and
    estimate, then a panic) and passes with the change.
    
    - `go test ./parquet/internal/encoding ./parquet/file
    ./parquet/pqarrow`: ok
    - `go test -tags noasm` and `GOARCH=386 go test` for
    `./parquet/internal/encoding`: ok; delta tests pass under `-race`
    - `go vet ./parquet/internal/encoding` and `gofmt -l`: clean
    
    This touches the same file as #1394 but different functions; the two
    merge cleanly.
    
    ### Are there any user-facing changes?
    
    No API changes. Delta-encoded columns with all-null pages are written
    smaller and with correctly sized pages.
---
 parquet/internal/encoding/delta_bit_packing.go |  5 +++
 parquet/internal/encoding/encoding_test.go     | 48 ++++++++++++++++++++++++++
 2 files changed, 53 insertions(+)

diff --git a/parquet/internal/encoding/delta_bit_packing.go 
b/parquet/internal/encoding/delta_bit_packing.go
index e0fbcbdd..3add6947 100644
--- a/parquet/internal/encoding/delta_bit_packing.go
+++ b/parquet/internal/encoding/delta_bit_packing.go
@@ -484,12 +484,17 @@ func (enc *deltaBitPackEncoder[T]) FlushValues() (Buffer, 
error) {
 
        buffer = buffer[:headerWriter.Written()]
        enc.totalVals = 0
+       enc.firstVal, enc.currentVal = 0, 0
 
        if enc.bitWriter != nil {
                flushed := enc.sink.Finish()
                defer flushed.Release()
 
                buffer = append(buffer, 
flushed.Buf()[:enc.bitWriter.Written()]...)
+               // Finish hands the sink's bytes to us, so this writer's 
position no longer
+               // matches the sink. The next Put creates a new writer; until 
then, a flush
+               // (for example of an all-null page) must write only the header.
+               enc.bitWriter = nil
        }
        return poolBuffer{memory.NewBufferBytes(buffer)}, nil
 }
diff --git a/parquet/internal/encoding/encoding_test.go 
b/parquet/internal/encoding/encoding_test.go
index 27ce54a2..a7009b2d 100644
--- a/parquet/internal/encoding/encoding_test.go
+++ b/parquet/internal/encoding/encoding_test.go
@@ -1043,6 +1043,54 @@ func TestDeltaBitPackedInt64DecodeAfterDiscard(t 
*testing.T) {
        }
 }
 
+// A column writer reuses one encoder for every page. A page whose values are 
all
+// null puts nothing, so its flush must produce just the empty-page header, 
exactly
+// as a fresh encoder would, and the next page must still round-trip.
+func TestDeltaBitPackedReuseAfterAllNullPage(t *testing.T) {
+       column := schema.NewColumn(schema.NewInt32Node("int32", 
parquet.Repetitions.Optional, -1), 1, 0)
+       flush := func(enc encoding.TypedEncoder) []byte {
+               buf, err := enc.FlushValues()
+               require.NoError(t, err)
+               defer buf.Release()
+               return bytes.Clone(buf.Bytes())
+       }
+       allNull := []int32{0, 0, 0}
+
+       fresh := encoding.NewEncoder(parquet.Types.Int32, 
parquet.Encodings.DeltaBinaryPacked, false, column, 
memory.DefaultAllocator).(encoding.Int32Encoder)
+       defer fresh.Release()
+       fresh.PutSpaced(allNull, []byte{0}, 0)
+       emptyPage := flush(fresh)
+
+       // 100,000 values make the first page larger than the sink's next 
buffer.
+       for _, n := range []int{3, 1000, 100_000} {
+               t.Run(fmt.Sprintf("first_page=%d", n), func(t *testing.T) {
+                       values := make([]int32, n)
+                       for i := range values {
+                               values[i] = int32(i*i + 7)
+                       }
+                       enc := encoding.NewEncoder(parquet.Types.Int32, 
parquet.Encodings.DeltaBinaryPacked, false, column, 
memory.DefaultAllocator).(encoding.Int32Encoder)
+                       defer enc.Release()
+
+                       enc.Put(values)
+                       flush(enc)
+
+                       enc.PutSpaced(allNull, []byte{0}, 0)
+                       assert.Zero(t, enc.EstimatedDataEncodedSize())
+                       assert.Equal(t, emptyPage, flush(enc))
+
+                       enc.Put(values[:3])
+                       page := flush(enc)
+                       dec := encoding.NewDecoder(parquet.Types.Int32, 
parquet.Encodings.DeltaBinaryPacked, column, 
memory.DefaultAllocator).(encoding.Int32Decoder)
+                       require.NoError(t, dec.SetData(3, page))
+                       out := make([]int32, 3)
+                       got, err := dec.Decode(out)
+                       require.NoError(t, err)
+                       require.Equal(t, 3, got)
+                       assert.Equal(t, values[:3], out)
+               })
+       }
+}
+
 func TestDeltaLengthByteArrayEncoding(t *testing.T) {
        column := schema.NewColumn(schema.NewByteArrayNode("bytearray", 
parquet.Repetitions.Required, -1), 0, 0)
 

Reply via email to