Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
34a12bd
[core] Prune manifest blocks with row-id sidecar indexes
leaves12138 Sep 11, 2026
e9796ae
[core] Coalesce manifest row-id index and block reads
leaves12138 Sep 11, 2026
d71c531
[test] Stabilize manifest sidecar regression tests
leaves12138 Sep 12, 2026
3803f7d
[core] Store manifest row-id sidecars in extra files
leaves12138 Sep 12, 2026
914897f
[core][python] Preserve cancellation through manifest index fallback
leaves12138 Sep 12, 2026
00b99d7
[core] Share manifest block indexes for partitions and row IDs
leaves12138 Sep 14, 2026
7e28f90
[core] Add nullable bucket payloads to manifest block indexes
leaves12138 Sep 14, 2026
5dd40f3
[core] Remove compatibility with draft manifest index formats
leaves12138 Sep 14, 2026
bc8f465
[core] Rename manifest block indexes to sidecars
leaves12138 Sep 14, 2026
06c4402
[core] Unify encoding for manifest sidecar payloads
leaves12138 Sep 14, 2026
95620cb
[core] Preserve manifest cache and explain statistics with sidecars
leaves12138 Sep 14, 2026
533ebe0
[core] Avoid redundant manifest sidecar interval decoding
leaves12138 Sep 14, 2026
56e362b
Fix minus
leaves12138 Sep 14, 2026
98b516b
[core] Use a byte budget for manifest sidecar metadata
leaves12138 Sep 14, 2026
4215e41
[core] Derive sidecar memory budget from manifest target size
leaves12138 Sep 14, 2026
e7bbc3e
[core] Short-circuit manifest sidecar predicate matching
leaves12138 Sep 14, 2026
853c45b
[core] Omit length fields for unavailable sidecar payloads
leaves12138 Sep 14, 2026
4e8776a
[core] Skip unused manifest sidecar payload decoding
leaves12138 Sep 14, 2026
d99bb14
Fix minus
leaves12138 Sep 14, 2026
a0e153b
[core] Share the manifest cache with sidecar reads
leaves12138 Sep 14, 2026
1306d7c
[core] Reuse SingleSegments for cached sidecar bytes
leaves12138 Sep 14, 2026
cd6ce62
[core] Remove fixed manifest sidecar header and block limits
leaves12138 Sep 14, 2026
6383bc6
[core] Use 4 MiB manifest sidecar read buffers
leaves12138 Sep 14, 2026
c300ff5
[python] Reuse local file cache for manifest sidecars
leaves12138 Sep 14, 2026
fc77243
[core] Reduce manifest sidecar read allocations
leaves12138 Sep 14, 2026
b2e63d3
[core] Select manifest sidecar payloads from table metadata
leaves12138 Sep 14, 2026
030fd58
[core] Require explicit manifest sidecar payload settings
leaves12138 Sep 14, 2026
a7841b0
[core] Default manifest sidecar switches to manifest sort
leaves12138 Sep 14, 2026
675393a
Fix minus
leaves12138 Sep 14, 2026
c00ac0c
[core] Pass sidecar settings to the manifest factory constructor
leaves12138 Sep 14, 2026
404895e
[core] Enable sidecar row-id payloads only for data evolution tables
leaves12138 Sep 14, 2026
75bdce9
[core] Derive manifest sidecar settings from table configuration
leaves12138 Sep 14, 2026
c5cee0a
[core] Handle sidecar budget boundaries without excess allocation
leaves12138 Sep 14, 2026
af7158f
[core] Merge master and preserve manifest sidecar references
leaves12138 Sep 14, 2026
510bcb3
[test] Remove manifest large-header test with shaded Avro conflict
leaves12138 Sep 14, 2026
3193bf7
[core] Fall back on manifest sidecar connection timeouts
leaves12138 Sep 14, 2026
321e33c
[core] Use a dedicated manifest sidecar cache segment
leaves12138 Sep 14, 2026
82b2fa9
[core] Simplify manifest sidecar read failure handling
leaves12138 Sep 14, 2026
31040cf
[core] Check row IDs before partition sidecar payloads
leaves12138 Sep 14, 2026
07bc780
[docs] Document manifest sidecar configuration options
leaves12138 Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions docs/docs/concepts/spec/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,162 @@ skip manifests before opening them.
Each extra file belongs exclusively to one manifest. It is retained and cleaned up together with
that manifest during snapshot, tag, or changelog deletion.

### Manifest Sidecar

With `manifest.sidecar.write` enabled, a manifest writer can create a binary
`<manifest-file-name>.avro.sidecar` sidecar. Its name is stored in the manifest-list
record's `_EXTRA_FILES`; the existing Avro schemas and `_VERSION` identifiers are unchanged.
Readers identify the sidecar by the `.avro.sidecar` suffix among these explicit
references, not by probing for a derived file name. Other extra-file references are preserved.

With `manifest.sidecar.read` enabled and a partition, row-ID or bucket filter available, readers can use
the sidecar to select complete Avro blocks before reading manifest entries. Each option
inherits `manifest-sort.enabled` when unset; an explicit value overrides it independently.
Since manifest sorting defaults to `false`, sidecar reads and writes are also disabled when
none of these options is set. Old manifests, null or empty extra-file lists, and lists containing only
other extra-file types use the normal manifest read path. Missing sidecars and explicit container
validation failures, such as unsupported versions, checksum mismatches and byte-budget violations,
also fall back to that path. Each block's partition, row-ID and bucket
coverage is independently usable; an unavailable dimension cannot exclude a block.
Java falls back only on `IOException`. If the current thread is interrupted, it instead throws
`UncheckedIOException` with the original I/O failure. Other exceptions and errors propagate unchanged;
Java does not inspect causes or suppressed exceptions. PyPaimon explicitly propagates cancellation
and interruption exception types, including wrapped causes.

Writers choose partition, row-ID and bucket payloads through internal sidecar settings derived from
the table. Partition coverage is enabled when the supplied partition count is greater than zero;
otherwise the partition dictionary and partition payloads are omitted. Row-ID coverage follows
`data-evolution.enabled`. Bucket coverage is enabled when `bucket` is not `-1`, independently
of data evolution. A disabled payload uses encoding 0, with no length or payload bytes.
Each payload remains independently usable by readers. These settings do not add
table options or prevent readers from using payloads already present in existing sidecars.

Version 1 uses the following layout. Container integers and payload integers
are fixed-width big endian. Encoding IDs are unsigned bytes with separate namespaces.

```text
magic : 8 bytes // ASCII PAIMSCAR
formatVersion : int // 1
manifestNameHash : 32 bytes // SHA-256 of the UTF-8 basename
manifestLength : long
manifestEntryCount : long // ADD + DELETE
avroHeaderLength : int
avroHeader : bytes // original schema, codec and sync marker
partitionCount : int
partitionDictionary[]
partitionByteLength : int
partitionBytes : bytes // existing manifest BinaryRow serialization
blockCount : int
blocks[] // original physical order
offset : long
length : long // complete encoded block, including sync marker
recordCount : long
partitionEncoding : byte
if partitionEncoding != 0:
partitionPayloadLength : int
partitionPayload : bytes
rowIdEncoding : byte
if rowIdEncoding != 0:
rowIdPayloadLength : int
rowIdPayload : bytes
bucketEncoding : byte
if bucketEncoding != 0:
bucketPayloadLength : int
bucketPayload : bytes
checksum : 32 bytes // SHA-256 of all preceding bytes
```

The block ID is its position. Its first entry ordinal is the sum of preceding record
counts and is not stored. Each complete partition tuple appears once in the dictionary,
including all its fields and nulls. The scan's partition type interprets the existing
serialized tuple. Partition predicates are evaluated once per dictionary entry.

| Dimension | Encoding | Payload |
| --- | --- | --- |
| Any | `0` | Unavailable; only the encoding byte is present. |
| Partition | `1` | Positive `partitionIdCount: int` followed by sorted unique dictionary IDs (`int`). |
| Row ID | `1` | Positive `rangeCount: int` followed by sorted disjoint inclusive `(start: long, end: long)` pairs. Coverage may conservatively include gaps. |
| Bucket | `1` | Positive `pairCount: int` followed by sorted unique `(bucket: int, totalBuckets: int)` pairs. |
| Any | Other nonzero ID | Skip exactly the bounded payload length; treat only this dimension as unavailable. |

Only nonzero encodings are followed by a length and payload. Payload lengths exclude
the encoding and length fields. Invalid lengths, known-payload
framing, dictionary references, interval order, checksums or physical coverage invalidate
the container. Byte spans must cover the entire original manifest after its header;
record counts must sum to the manifest entry count. Readers validate the checksum,
payload framing (including known count/length consistency), and the complete block directory
even when a block is rejected. Block payload contents are decoded and validated only for
dimensions still needed by the filters.

Bucket encoding 1 contains a positive `pairCount: int` followed by that many
`(bucket: int, totalBuckets: int)` pairs. Pairs are sorted by bucket, then totalBuckets,
and deduplicated. They preserve bucket-count changes between writes; the bucket number
alone is not sufficient for point lookup after rescaling. A valid pair satisfies
`0 <= bucket < totalBuckets`. Missing, invalid, negative/synthetic or over-budget bucket
metadata makes that block's bucket coverage unavailable (encoding 0, no length or payload). Partition
and row-ID coverage remain independently usable; no mutual-exclusion restriction is imposed.

Readers test bucket-only queries using the existing bucket-selection logic, including
the total-bucket count. Java uses conservative partition-independent bounds for
`ManifestBucketFilter`; arbitrary partition-dependent callbacks remain at the entry
filter stage. An unavailable bucket payload cannot exclude a block. Malformed payload lengths,
pair counts, ordering or values invalidate the container rather than excluding a block.

All entries contribute, including ADD, DELETE and every file format/column group.
Row-ID ranges are never expanded into individual values. If an exact union exceeds its
available byte budget, it becomes the inclusive `[min,max]` envelope with encoding 1. Processing
continues through the end of the block to extend those bounds and detect unknown row IDs.
An unknown or invalid row-ID range makes only that block's row-ID payload unavailable.
Partition budget exhaustion independently makes that block's partition payload unavailable.
The dictionary can consequently be incomplete for the manifest: a dictionary miss never
excludes a block with unavailable partition coverage. Later blocks can still use existing IDs.

`manifest.sidecar.max-bytes` bounds the whole serialized container, including the
partition dictionary and all three payload types. It accepts memory sizes such as
`16 mb` and, when unset, defaults to twice the configured `manifest.target-file-size`
(16 MiB with the default 8 MiB manifest target). An explicit sidecar size overrides this default.
The effective budget is capped at 2147483646 bytes to fit the in-memory byte-array representation.
Smaller budgets that cannot fit a sidecar skip this optimization; they do not prevent manifest writes
or reads. Doubling a very large manifest target saturates at the maximum representable memory size.
The Avro header and block directory share this byte budget without separate size or count limits.
Writers discard optional row-ID payloads, bucket payloads, then partition payloads/dictionary if necessary,
to fit the complete directory. If the directory itself cannot fit, no sidecar is published.
No emitted sidecar omits block descriptors. These are encoded-size bounds; Avro header parsing
and sidecar construction also incur object/buffer overhead. Query concurrency multiplies per-reader costs.

For conjunctive filters a block is retained only if each dimension is either unavailable
or matches. Within each block, matching tests row ID, partition, then bucket coverage.
It skips absent filters and short-circuits after a dimension rejects a block, skipping
the contents of later payloads. Matches in different dimensions can come
from different entries in the block, so entry filtering and deletion merging remain
necessary. Block min/max is derived from the first/last interval before testing the
individual intervals.

Readers still consume and validate the whole bounded sidecar. A partition-only query
therefore reads row-ID payload bytes too; payload lengths save decoding work for unknown
encodings, not storage I/O. Selected compressed blocks are read by byte range with adjacent
spans coalesced. Existing immutable manifests are not backfilled by enabling the write option.

Java readers share the existing manifest cache for complete sidecar bytes, keyed by the
explicit sidecar path and subject to the same memory budget and single-file threshold.
Only successful reads and selections populate the cache. Each query creates independent
views and reapplies its filters and byte budget; query-specific selections are not cached.

PyPaimon reuses `CachingFileIO` for sidecar bytes. Enable `local-cache.enabled` on the catalog
and include `meta` in `local-cache.whitelist` (included by default). Files ending in
`.avro.sidecar`, including custom names, use the same cache as other metadata. The cache
stores raw byte blocks by full path and block index, sharing `local-cache.max-size` and
`local-cache.block-size`. Without `local-cache.dir` it uses memory; setting that option
enables disk caching. Sidecar validation, filters and the read byte budget are reapplied
on every query. Local caching is disabled by default.

Java selections covering every block can reuse the full-manifest cache; partial selections
bypass it. PyPaimon explain scans disable sidecar pruning to preserve complete entry counters.

Selected blocks still pass through entry filtering and ADD/DELETE reconciliation. Snapshot,
tag, changelog, orphan-file and failed-commit cleanup retain or remove the sidecar through
its extra-file reference together with the owning manifest.

## Manifest

Data manifests record **ADD** (`0`) and **DELETE** (`1`) entries. Readers reconcile these entries
Expand Down
18 changes: 18 additions & 0 deletions docs/generated/core_configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,24 @@
<td>Boolean</td>
<td>Whether to skip automatic manifest merging during commit when write-only is true. This also skips automatic manifest sort rewrite. Explicit manifest compaction is not affected.</td>
</tr>
<tr>
<td><h5>manifest.sidecar.max-bytes</h5></td>
<td style="word-wrap: break-word;">(none)</td>
<td>MemorySize</td>
<td>Maximum serialized manifest sidecar size, including header and checksum. Defaults to twice manifest.target-file-size. Optional payloads are dropped before omitting a sidecar whose complete block directory cannot fit.</td>
</tr>
<tr>
<td><h5>manifest.sidecar.read</h5></td>
<td style="word-wrap: break-word;">(none)</td>
<td>Boolean</td>
<td>Read optional manifest sidecars for partition, row-id or bucket filters after coarse pruning. Missing or invalid sidecars fall back to manifest reads. Defaults to manifest-sort.enabled when unset.</td>
</tr>
<tr>
<td><h5>manifest.sidecar.write</h5></td>
<td style="word-wrap: break-word;">(none)</td>
<td>Boolean</td>
<td>Write sidecars with independent partition, row-id and bucket coverage for newly created manifests. Defaults to manifest-sort.enabled when unset.</td>
</tr>
<tr>
<td><h5>manifest.target-file-size</h5></td>
<td style="word-wrap: break-word;">8 mb</td>
Expand Down
39 changes: 39 additions & 0 deletions paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,27 @@ public InlineElement getDescription() {
+ "in the previous file. This must not exceed "
+ "'variant.shredding.minFieldCardinalityRatio'.");

public static final ConfigOption<Boolean> MANIFEST_SIDECAR_WRITE =
key("manifest.sidecar.write")
.booleanType()
.noDefaultValue()
.withDescription(
"Write sidecars with independent partition, row-id and bucket coverage for newly created manifests. Defaults to manifest-sort.enabled when unset.");

public static final ConfigOption<Boolean> MANIFEST_SIDECAR_READ =
key("manifest.sidecar.read")
.booleanType()
.noDefaultValue()
.withDescription(
"Read optional manifest sidecars for partition, row-id or bucket filters after coarse pruning. Missing or invalid sidecars fall back to manifest reads. Defaults to manifest-sort.enabled when unset.");

public static final ConfigOption<MemorySize> MANIFEST_SIDECAR_MAX_BYTES =
key("manifest.sidecar.max-bytes")
.memoryType()
.noDefaultValue()
.withDescription(
"Maximum serialized manifest sidecar size, including header and checksum. Defaults to twice manifest.target-file-size. Optional payloads are dropped before omitting a sidecar whose complete block directory cannot fit.");

public static final ConfigOption<String> MANIFEST_COMPRESSION =
key("manifest.compression")
.stringType()
Expand Down Expand Up @@ -3217,6 +3238,24 @@ public MemorySize manifestTargetSize() {
return options.get(MANIFEST_TARGET_FILE_SIZE);
}

public boolean manifestSidecarWriteEnabled() {
return options.getOptional(MANIFEST_SIDECAR_WRITE).orElseGet(this::manifestSortEnabled);
}

public boolean manifestSidecarReadEnabled() {
return options.getOptional(MANIFEST_SIDECAR_READ).orElseGet(this::manifestSortEnabled);
}

public MemorySize manifestSidecarMaxSize() {
return options.getOptional(MANIFEST_SIDECAR_MAX_BYTES)
.orElseGet(
() -> {
long target = manifestTargetSize().getBytes();
return new MemorySize(
target > Long.MAX_VALUE / 2 ? Long.MAX_VALUE : target * 2);
});
}

public MemorySize manifestFullCompactionThresholdSize() {
return options.get(MANIFEST_FULL_COMPACTION_FILE_SIZE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.apache.paimon.manifest.ManifestEntry;
import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestFileMeta;
import org.apache.paimon.manifest.ManifestSidecar;
import org.apache.paimon.operation.ManifestFileMerger;
import org.apache.paimon.options.Options;
import org.apache.paimon.schema.FileSystemSchemaManager;
Expand Down Expand Up @@ -300,7 +301,9 @@ private ManifestFile createManifestFile() {
"zstd",
pathFactory,
TARGET_MANIFEST_SIZE,
null)
null,
new ManifestSidecar.Settings(
new CoreOptions(new Options()), PARTITION_TYPE.getFieldCount()))
.create();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.apache.paimon.manifest.IndexManifestFile;
import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestList;
import org.apache.paimon.manifest.ManifestSidecar;
import org.apache.paimon.metastore.AddPartitionCommitCallback;
import org.apache.paimon.metastore.AddPartitionTagCallback;
import org.apache.paimon.metastore.ChainTableCommitPreCallback;
Expand Down Expand Up @@ -211,7 +212,8 @@ public ManifestFile.Factory manifestFileFactory() {
options.manifestCompression(),
pathFactory(),
options.manifestTargetSize().getBytes(),
readManifestCache);
readManifestCache,
new ManifestSidecar.Settings(options, partitionType.getFieldCount()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ public boolean test(BinaryRow partition, int bucket, int totalBucket) {
|| totalAwareBucketFilter.test(partition, bucket, totalBucket);
}

/** Conservatively checks an indexed pair without inventing a partition for custom filters. */
public boolean mayContain(int bucket, int totalBuckets) {
if (onlyReadRealBuckets && bucket < 0) {
return false;
}
if (specifiedBucket != null && bucket != specifiedBucket) {
return false;
}
if (bucketFilter != null && !bucketFilter.test(bucket)) {
return false;
}
return !(totalAwareBucketFilter instanceof ManifestBucketFilter)
|| ((ManifestBucketFilter) totalAwareBucketFilter)
.mayContain(bucket, bucket, totalBuckets);
}

/** Conservatively tests whether a manifest's bucket metadata can contain a matching entry. */
public boolean mayContain(ManifestFileMeta manifest) {
Integer minBucket = manifest.minBucket();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public final class ManifestAvroReader implements AutoCloseable {
}
}

public byte[] headerBytes() {
return blockReader.headerBytes();
}

public long blockOffset() {
return blockReader.blockOffset();
}

public long blockLength() {
return blockReader.blockLength();
}

/** Returns whether another raw Avro block is available. */
public boolean hasNext() throws IOException {
return blockReader.hasNextBlock();
Expand Down
Loading
Loading