Storage format 2.3 · Design sketch
One page layout, two planes.
Lance 2.2 picks a page layout from a list — MiniBlock, FullZip, Constant — and the sparse work adds another entry. This rework replaces the list with a product: every page is a structural plane times a value plane, and the old layouts become cells of the matrix. The bytes on disk do not move.
Every structural idea became another layout.
In 2.1/2.2 the PageLayout oneof grows one variant per idea, and each variant re-implements its surroundings. Dense rep/def — a single structural representation — is already stored three different ways.
FullZipLayout
2.1ConstantLayout
2.2SparseLayout
#7628Three copies of the same dense representation, then a fourth silo that re-implements the chunked value machinery minus dictionary — and welds sparse structure to narrow values. On this trajectory, every structural-representation × value-storage combination costs a hand-written layout. The next idea would be a fifth.
A page is structure × values.
The 2.3 proto has no layout variants left. PageLayout holds the two planes directly; each plane is a small oneof of codecs with genuinely different cost profiles.
message PageLayout { StructuralPlane structure = 1; ValuePlane values = 2; uint64 num_items = 3; uint64 num_visible_items = 4; } message StructuralPlane { oneof codec { DenseRepDef dense = 1; SparseStructure sparse = 2; // as specified in #7631 } } message DenseRepDef { // pure logical semantics, zero physical fields repeated RepDefLayer layers = 1; } message ValuePlane { oneof codec { ChunkedValues chunked = 1; ZippedValues zipped = 2; ConstantValue constant = 3; NoValues none = 4; } }
Structural plane rows ↔ visible items
Declares the nesting shape and rebuilds rep/def on read. Everything invisible — nulls, empty lists — is its job. Dense addresses through the repetition index or control words; sparse addresses through its own position sets.
Value plane visible items → bytes
Stores exactly num_visible_items visible leaf values and maps them to bytes: chunk metadata for chunked, stride or offset index for zipped, an inline scalar for constant, nothing for none.
Aliases stay API unbroken
structural_encoding = "miniblock" forces dense × chunked; "fullzip" forces dense × zipped — the exact semantics those names have today, which also suppresses automatic sparse selection.
Five layouts become eight cells.
Every existing layout lands in a cell with its bytes unchanged; three cells are new capability that previously would each have cost another layout. Click a cell for the details.
Pick a cell
Each cell describes one combination of structural codec and value codec. Colors mark whether it maps to an existing 2.2 form, re-mounts the landed SparseLayout, or is capability the matrix unlocks for free.
Placement is derived, not encoded.
Structural data has two consumption modes, and each mode fixes where its bytes live. Nothing about placement is a choice a writer records — it follows from the pair of codecs.
Index-like · sparse sets
page-levelO(sparsity) small: read once at page open, then all addressing happens in memory. Structure first, then only the value bytes a take actually reaches. Empty takes do zero IO.
Payload-like · dense levels
rides the value storeO(items) large: reading levels up front would read the page twice, so they ride the value store at its IO granularity — inside each chunk, or as per-record control words. That is what keeps nested cold random access at one IO, and it is exactly where 2.1 already puts them.
Streams are declared by layers, carried by the value codec
A list layer implies a rep stream (even all-valid lists have variable-length offsets); a nullable layer implies a def stream. DenseRepDef carries only layers — each value codec's DenseLevels sub-message describes how its own storage carries the declared streams, and readers validate the two by derivation. There is no degree of freedom to coordinate between the planes.
A refactor claims refactor benefits.
This proposal ships after SparseLayout (#7628) lands and builds on it. Being honest about what belongs to whom keeps the review focused.
No new encoding
The sparse performance numbers — 26.5x to 334x smaller on the benchmark shapes — ship with #7628, not here. Existing pages keep their exact bytes; kept combinations are verified byte-for-byte against 2.2 and against SparseLayout pages.
The delta is the matrix
Three unlocked cells — sparse × zipped for mostly-null wide values, sparse × constant, sparse × none — plus dictionary composing with sparse structure. Each of these would otherwise have been another hand-written layout.
One rejected alternative
A fully separated, self-owned level store (ORC-style) would make the planes physically independent too — but it turns one-IO nested takes into two, or requires new chunk byte formats. Expression cleanliness alone did not clear that bar.
Old files keep working; stable 2.3 freezes the planar model.
The reader normalizes every pb21 form into the same internal planar representation, so one decode framework serves all versions. 2.3 is still unstable — the rework must land before it stabilizes.
pb2.0 · frozen
2.0 files have no page-layout concept — structure lives in the encoding tree. They keep their existing frozen legacy path and stay out of normalization.
pb21 · normalized
2.1/2.2 files, plus 2.3-unstable files written with the interim SparseLayout, all map into planar cells with bytes unchanged. Complex constant/all-null pages read as legacy states.
pb23 · planar
New 2.3 files carry the planar pair. Complex constant/all-null pages switch to sparse sets — the one deliberate behavior change. Stable 2.3 freezes this model, not four layout variants.
- Old readers reject 2.3 files through the existing file-version gate — nothing depends on proto field-level compatibility.
structural_encodingkeeps its values: "miniblock" and "fullzip" force the historical dense combinations.- Blob v1 pages stay readable; the v1 write path closes in 2.3 with an explicit error pointing at blob v2.
- Rollout is two reviewable steps: the planar rework with byte-for-byte acceptance, then sparse × zipped for wide values.
Context: proposal discussion #7635 · sparse layout discussion #7631 · PR #7628 · companion sketch: Lance discrete sparse layout