-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[core] Prune manifest blocks with row-id sidecar indexes - #9743
[core] Prune manifest blocks with row-id sidecar indexes #9743leaves12138 wants to merge 5 commits into
Conversation
Add optional bounded block indexes and Java/PyPaimon pruning. Publish explicit index references in manifest metadata and preserve them through serialization, rewrites, commit cleanup, snapshot retention, and orphan collection.
Use bounded 1 MiB read requests in Java and Python to avoid object-store request amplification. Merge adjacent selected blocks into spans and buffer Java reads independently of the Avro consumer read size. Add regression tests for request counts, skipped gaps, short reads, size budgets, stream closure and truncated inputs.
JingsongLi
commented
Sep 11, 2026
I suggest using a single manifest index sidecar organized by Avro block. Partition information would support partition predicate pushdown during planning. Since both partition and row-id information describe the same blocks, they can live in the same block record and share its physical location.
A possible layout is:
Header
formatVersion
manifest identity (name hash, file length, entry count)
original Avro header
Partition dictionary
partitionId -> complete partition tuple
blockCount : int
BlockIndexRecord[] // original manifest order
offset : long // byte offset in the manifest
length : long // complete Avro block length
recordCount : long // number of manifest entries
flags : byte // independent availability bits
[if ROW_ID_AVAILABLE]
rangeCount : int
ranges : (start: long, end: long)[] // inclusive interval unions
[if PARTITION_AVAILABLE]
partitionIdCount : int
partitionIds : int[] // sorted and deduplicated
Checksum of all preceding bytes
The partition dictionary is shared across the file and can reuse the existing manifest partition encoding, preserving full tuples, types and nulls. Each block only stores dictionary IDs. The block ID is implicit in its position; firstRecord can be derived from preceding recordCount values.
The two indexes should remain independently usable within each block:
- An availability bit means that the corresponding information completely covers the block's entries, including both ADD and DELETE entries and all column groups.
- If row-id coverage is unknown or exceeds its budget, omit that block's row-id payload while retaining its partition information. Apply the same rule independently to partition information.
- An unavailable index means "cannot prune using this index," rather than an empty result. Invalid file metadata or a checksum failure should fall back to the normal manifest read.
During planning, evaluate the partition predicate against the dictionary once, then check each block's partition IDs and row-id intervals. For conjunctive filters, intersect their candidate block sets. Read the selected blocks and retain the existing entry filtering and ADD/DELETE merge, since block-level matches do not guarantee that the same entry satisfies both predicates.
This layout assumes reading the whole sidecar, as the current implementation does. A partition-only query would also read the row-id index bytes. I would start with this simpler layout and consider separate physical sections if measurements show that selective index reads materially improve planning time.
Forward selected_blocks through the append-only reader test wrapper. Fix the manifest target size and assert explicit retained and expired manifest sets so snapshot and tag retention coverage does not depend on randomized file sizes.
Merge current master and adopt its manifest extra-files metadata instead of a dedicated index-file-name field. Discover row-id indexes through explicit suffixed references and preserve other extra files across reads and cleanup. Verify Java/Python compatibility, mixed extra-file references, retention and failed-commit cleanup. Java core: 151 tests passed. Python: 106 passed, 4 skipped. Random interval and byte checks: 18000 queries passed.
Propagate PyArrow cancellations and inspect chained and suppressed failures before falling back to full manifests. Preserve Java interruption state and fatal failures, guard against exception cycles, and cover stream open/read/close behavior with regression tests.
JingsongLi
commented
Sep 12, 2026
|
Here is a refined version of the block-oriented layout, keeping the file-level partition dictionary and making each block's two payloads independently extensible. The dictionary stores each complete partition tuple once, using the existing manifest partition serialization. This preserves tuple values and nulls; the scan's existing The encoding bytes identify how to decode the corresponding payload, with separate ID namespaces for partition and row-id payloads. They replace the availability flags:
The container's integers and the encoding-1 payload integers use fixed-width big-endian representation; partition bytes retain their existing serialization. Encoding bytes are interpreted as unsigned IDs. Each payload length counts only its payload bytes, excluding the encoding and length fields. Other nonzero encoding IDs are reserved for future representations. If a reader does not recognize one, it skips exactly that payload length and treats that dimension as unavailable, while still being able to use the other dimension. Lengths must be bounded and validated. The outer For example, There are several important correctness and budget rules:
For conjunctive partition and row-id filters, select each block using: Only an empty candidate block set permits skipping the manifest. Selected blocks still pass through the existing entry filtering and ADD/DELETE merge. This keeps one sidecar and one record per block. It still assumes a bounded whole-sidecar read: payload lengths allow skipping decoding and unknown encodings, but do not by themselves save storage I/O. Index size, block selectivity and planning latency should determine whether selective physical reads are worthwhile later. |
Uh oh!
There was an error while loading. Please reload this page.
Purpose
Row-ID filters can retain manifests whose coarse min/max overlaps the query even when only a few Avro blocks contain relevant entries. Add optional row-id sidecars that select complete blocks before the existing manifest entry filtering and ADD/DELETE merge.
_EXTRA_FILESmanifest metadata introduced by [core][python] Add extra files to manifest metadata #9746 in Java and PyPaimon, without adding a dedicated index field. Identify.row-id-indexsidecars only among explicit extra-file references and preserve unrelated extra files. Null, empty, and other-extra-only metadata do not probe for an index.Both
manifest.row-id-index.writeandmanifest.row-id-index.readdefault to false. Java builds each index by projecting the final manifest after it closes; existing immutable manifests are not backfilled.Tests
Java 8, with normal Maven checks enabled after merging master: 153 core tests passed.
Python: 113 tests passed, 4 skipped.
Coverage includes shared Java/Python format fixtures, 64-bit interval boundaries, block offsets and ordinals, selective body reads, ADD/DELETE correctness, legacy metadata compatibility, explicit suffixed index names, mixed extra-file references, no-index/no-query behavior, conservative fallback, rolling/rewrite failures, commit cleanup, snapshot/tag protection and orphan collection. The manifest tests also retain the upstream cache-size and bucket-filter regression coverage.
Additional local checks passed: 18,000 randomized interval queries across 3,000 manifests; Java-to-Python-to-Java manifest-list interoperability for five extra-file variants; direct and wrapped Java cancellation at open/read/close. Cancellation tests also cover close errors masking earlier failures, suppressed interruption/fatal errors and cyclic exception graphs.