Skip to content

parquet/pqarrow: DataPageV2 writer can split repeated byte-array/list values across page boundaries #882

Description

@punkeel

We hit this in production with Parquet files written by a Go writer and read by Rust/DataFusion. The original bad file is large and contains PII, so it cannot be shared. The repro below is synthetic and minimal.

Repro repository: https://github.com/punkeel/parquet-repro-1

The repro uses Arrow Go's public writer path:

pqarrow.NewFileWriter(...)
writer.WriteBuffered(record)

Minimal shape:

c1: required list<nullable bool>

row 0: [true, null]
row 1: [true]

The actual null element is important: it makes pqarrow use the WriteBatchSpaced path. If the element is nullable/optional but the data has no nulls, this tiny repro does not hit the same path.

With DataPageV2 enabled, Arrow Go writes page 2 starting with repetition level 1, i.e. inside row 0's list:

OK  c1 page 01: num_rows=1 num_values=1 first_rep=0 reps=[0]
BAD c1 page 02: num_rows=1 num_values=2 first_rep=1 reps=[1 0]

That appears to violate the Parquet DataPageV2 rule. The spec says in parquet.thrift:

Every page must begin at a row boundary (repetition_level = 0): rows must not be split across page boundaries when using V2 data pages.

To Reproduce

git clone https://github.com/punkeel/parquet-repro-1
cd parquet-repro-1

go run . -out repro.parquet
go run ./cmd/inspect -in repro.parquet

Observed inspector output:

inspecting repro.parquet
column 0: c1.list.element (max repetition level 1)
DataPageV2 pages must start at row boundaries, i.e. first_rep must be 0.
OK  c1 page 01: num_rows=1 num_values=1 first_rep=0 reps=[0]
BAD c1 page 02: num_rows=1 num_values=2 first_rep=1 reps=[1 0]

FOUND: this file contains DataPageV2 pages for repeated c1 that start with rep_level != 0.
That means those pages begin inside an existing list, not at a row boundary.

The checked-in repro.parquet is 544 bytes:

sha256: 623e19d44b23c1855c225e60aa8027e0c8d1456cd51210f475643b5277b57579

This also reproduces with a larger batch boundary:

go run . -out /tmp/repro-bool-long.parquet -list-len 20001 -batch-size 20000
go run ./cmd/inspect -in /tmp/repro-bool-long.parquet

That prints:

OK  c1 page 01: num_rows=1 num_values=20000 first_rep=0 reps=[0 1 1 1 1 1 1 1 1 1 1 1]
BAD c1 page 02: num_rows=1 num_values=2 first_rep=1 reps=[1 0]

Expected behavior

For repeated columns written as DataPageV2, Arrow Go should only start pages at repetition_level = 0. If the writer cannot satisfy that for WriteBatchSpaced, it should either align the split to a row boundary or return an error instead of writing a non-compliant file.

Version information

github.com/apache/arrow-go/v18 v18.6.0
Go module directive: go 1.25.5
Local Go tool used for the repro: go1.26.4 darwin/arm64

The production writer also used:

github.com/apache/iceberg-go v0.6.0

The repro omits Iceberg because the invalid bytes are emitted by the Arrow Go Parquet writer after the Arrow record/schema already exists.

Related

This was originally noticed via an Arrow Rust reader failure: apache/arrow-rs#10243. That issue is expected to be closed or treated as reader-side clarification because the original file is non-compliant. The remaining bug is on the Arrow Go writer side.

Suspected source

In github.com/apache/arrow-go/v18 v18.6.0, the regular batch path has DataPageV2 row-boundary handling for repeated columns. The analogous WriteBatchSpaced path appears not to apply the same alignment. pqarrow uses WriteBatchSpaced when nullable values or nullable-parent state must be preserved, which is why the actual null in this repro is the key trigger.

[cc @truffle-dev @etseidl for visibility]

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions