Skip to content

GH-530: Add modular footer array-page specification - #611

Closed
Jiayi-Wang-db wants to merge 7 commits into
apache:masterfrom
Jiayi-Wang-db:modular-footer-array-pages
Closed

GH-530: Add modular footer array-page specification#611
Jiayi-Wang-db wants to merge 7 commits into
apache:masterfrom
Jiayi-Wang-db:modular-footer-array-pages

Conversation

@Jiayi-Wang-db

Copy link
Copy Markdown

Rationale for this change

The traditional array-of-structs footer requires readers to walk metadata for every column even
when a query projects only a few columns. The modular footer transposes metadata into independent
column-major modules, but storing every encoded array as a Thrift binary field obscures the
boundary between the Thrift control structures and the actual array encoding.

This draft explores an explicit metadata-page model analogous to Parquet data pages: a
Compact-Thrift header followed by a raw, uncompressed array payload. It keeps placement,
statistics, and page indexes as independent modules and supports projected random access without
coupling placement to optional statistics.

What changes are included in this PR?

  • Add the base modular-footer data model and its README entry.
  • Add a detailed array-page proposal covering framing, indexing, compatibility, validation, and
    evaluation criteria.
  • Add normative Thrift definitions for metadata-page headers and directories.
  • Define BIT_PACKED for dense arrays and SPARSE for sparse optional values.
  • Define a two-level directory so page-index metadata does not expand the always-read root.
  • Keep metadata-page payloads uncompressed and outside Thrift fields.

Do these changes have PoC implementations?

Not yet. This is a draft specification intended to establish the wire model before updating the
existing modular-footer PoC. The proposal lists the required size, projection, sparse-statistics,
malformed-input, and cross-implementation evaluations.

Jiayi-Wang-db and others added 7 commits August 21, 2026 20:36
A struct-of-arrays, column-major, modular replacement for FileMetaData that lets a
reader decode work proportional to the projection instead of the table width.

- Small always-read core (footer scalars, inline schema and placement, and a
  directory); optional modules (row-group stats, offset index, column index, file
  metadata) located by a directory of absolute file offsets.
- The page index lives outside the footer; offset index and column index are separate
  modules, each located via a per-column PageIndexDirectory (relative cumulative
  offsets), so a projection fetches only its columns' blobs.
- BITPACK integer arrays for O(1) column-selective random access; VarLenColumn for
  min/max with common-prefix dedup and inexact-truncation flags.
- Forward compatibility via Parquet versioning; adoption targets the sole footer;
  encryption keeps placement plaintext and encrypts per-column statistics.

Co-authored-by: Isaac
Make the offset index and column index per column chunk (per (column, row
group)), matching standard Parquet's per-chunk OffsetIndex/ColumnIndex, instead
of one blob per leaf column spanning all row groups:

- OffsetIndexColumn/ColumnIndexColumn -> OffsetIndexChunk/ColumnIndexChunk, each
  holding a single chunk's per-page arrays.
- Drop first_page_index: each blob is one chunk, so its parallel per-page arrays
  self-delimit (their common length is the page count); no page-count field.
- boundary_orders (per-row-group array) -> boundary_order (a single
  parquet.BoundaryOrder scalar per chunk).
- PageIndexDirectory.column_offsets (num_columns+1) -> chunk_offsets
  (num_chunks+1, column-major), so a reader fetches page index only for surviving
  (column, row group) chunks after row-group pruning; a whole column's chunks stay
  contiguous for a single range fetch.

Co-authored-by: Isaac
Clarify why the presence bitsets exist: a column-oriented positional array cannot
distinguish a genuine 0 (or empty bytes) from an unset value, so an optional field
pairs its value array with a presence bitset (has_null_count, has_minmax, ...) that
records which entries are actually set -- restoring the optional/absent semantics
the array-of-structs FileMetaData gets from Thrift field presence. Also note in the
bitset definition that a bitset serves two roles: boolean data (null_pages,
is_fully_dict_encoded) vs a presence flag gating a paired value array.

Co-authored-by: Isaac
…tatsMatrix -> RowGroupStatsMatrix

No module is carried inline anymore. ModularFooter now holds only the
footer-level scalars plus a required directory[] that locates every module
(SCHEMA and PLACEMENT always present; the rest optional). Also renames the
stats module RgStatsMatrix -> RowGroupStatsMatrix and the enum value
RG_STATS -> ROW_GROUP_STATS.

Co-authored-by: Isaac <no-reply@databricks.com>
…y_dict_encoded docs

- Header: per-chunk (column-major) unless noted; numeric arrays BITPACK-encoded
  for column-selective random access.
- Explain first_dict_page + dictionary_page_offsets: a flat offset array plus a
  per-chunk cumulative (CSR) index, so a chunk can hold zero, one, or several
  dictionary pages without a format change.
- Explain why is_fully_dict_encoded replaces ColumnMetaData.encoding_stats: the
  fast path only needs whether the whole chunk is dictionary-encoded, so a single
  per-chunk bit replaces the variable-length list<PageEncodingStats>.

Co-authored-by: Isaac <no-reply@databricks.com>
…s evidence

Keep the single BITPACK encoding (simplest) but document the encoding goals
(random access, align with Parquet encodings, minimal size) and the plan to
benchmark candidate encodings against real fleet footers before adopting one.
Motivated by a real 2.29M-chunk footer where sparse min/max full-length offsets
cost ~10 MB to index ~0.44 MB of values.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Jiayi <wjyi2015@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant