FullZip
dense eventsOne structural stream covers the full row range. Take can hit a broad compressed page even when selected rows are empty.
Storage format 2.3 · Design sketch
Rep/def streams describe every structural event in row order. That is correct, but sparse nested data pays for empty rows again and again. The discrete sparse layout keeps logical row coordinates, then stores only the structural facts that lead to real child values.
A sparse nested column can have very few values but a huge logical row domain. If the format still writes structure as a dense event stream, empty rows keep participating in compression, decoding, and random access.
One structural stream covers the full row range. Take can hit a broad compressed page even when selected rows are empty.
The dense structure is split into smaller units. Random access improves, but empty regions still shape the page plan — and a sparse enough page exceeds the structural budget entirely.
Structure is stored as sparse mappings over each parent slot domain. Payload pages only contain reachable leaf values.
The outermost slot domain is the page row domain. Every nested layer maps selected parent slots to child slots. Leaf value buffers live in the final compact value domain.
Row ids are not rewritten. The format keeps the page row domain so slices and takes preserve Arrow semantics.
A list layer records non-empty parent positions and child counts. Missing valid positions mean empty lists. When every non-empty list has the same length, a constant count set replaces the counts buffer.
Nullable layers record null positions in their own parent slot domain, not in a global event stream.
A fixed-size-list layer stores only its dimension and null positions. Child ranges are deterministic, so no positions or counts are written.
Value buffers contain only visible leaf values and keep mini-block compression for the physical payload.
list<int>.This is the smallest example that shows the contract: empty lists are represented by absence from sparse structural facts, not by payload gaps.
Input rows are converted into a list structural layer, and the leaf payload is packed independently.
There is no special global row-index trick. Each layer owns a parent slot domain and emits the child ranges needed by the next layer.
struct<profile: struct<events: list<struct<score:int32, tags:list<int32>>>>>The selected rows either stop at a null or empty layer, or continue downward to compact score and tag payloads. In the file, each leaf column (score, tags) is its own page repeating these shared outer layers — the diagram shows one merged view.
The important change is not just fewer pages. It is the ability to decide whether a selected path exists before reading and decoding value payloads.
Empty rows do not produce dense rep/def events. Sparse positions and counts scale with non-empty structure, not with total row count.
Sequential reads walk compact structural metadata and compact value buffers. There is less structural data to inflate before rebuilding Arrow arrays.
Take first slices slot-domain mappings. Empty and null branches stop in metadata; non-empty branches become exact leaf ranges.
Sparse is a page layout, not a column-level promise. The writer decides per page; readers identify it only from PageLayout.sparse_layout, never from field metadata.
Field metadata forces sparse layout for the column's pages. Requests error out — never silently fall back — on file versions before 2.3, or when the column has no native structural layers (or uses dictionary / packed-struct blocks).
With no explicit request, the writer switches to sparse when the dense rep/def stream exceeds the mini-block structural page budget — the page would need splitting, or a single row alone is over budget — and the page's structure has a native sparse mapping.
When the structural budget is satisfied, the existing mini-block / full-zip selection path is unchanged. Dense data never pays for the sparse machinery.
This layout belongs to file version 2.3 because it changes the physical representation of nested structure. Readers must reject malformed sparse pages with a format error.
Value chunk metadata: one 8-byte entry per chunk — packed chunk size and the number of visible values. Sums must match the value buffer size and num_visible_items exactly.
Mini-block encoded value chunks. Leaf payload only — no rep/def levels inside.
valuesStructural buffers — one per explicit position or count set — in SparseStructuralLayer order, outermost to innermost.