diff --git a/docs/docs/concepts/spec/manifest.md b/docs/docs/concepts/spec/manifest.md
index 29466c1ad683..7f8bf7a8a5e7 100644
--- a/docs/docs/concepts/spec/manifest.md
+++ b/docs/docs/concepts/spec/manifest.md
@@ -72,20 +72,28 @@ using independent partition, row-ID and bucket coverage. A sidecar uses the
derived file name. The Avro schemas and `_VERSION` identifiers remain unchanged.
The utility includes construction, validation, block selection and optional caching. Java table
-writers generate sidecars when `manifest.sidecar.enabled` is true; when unset, it inherits
+writers and scans use sidecars when `manifest.sidecar.enabled` is true; when unset, it inherits
`manifest-sort.enabled`. Both ordinary writes and raw manifest rewrites build the sidecar from
the completed output manifest and publish its `_EXTRA_FILES` reference only after both files
close successfully. Failed writes and aborted writers clean up their own manifest/sidecar pairs.
-Scans do not yet invoke sidecar pruning automatically. Callers remain responsible for applying
-entry filters and reconciling ADD/DELETE entries after block selection. The low-level `build`
-method returns sidecar bytes without writing or publishing another file.
+Scans with partition, row-ID or bucket filters select blocks before reading manifest entries.
+Normal entry filtering and ADD/DELETE reconciliation still apply. Missing or unusable sidecars
+fall back to normal manifest reads; disabled sidecars and scans without these filters do not
+perform sidecar I/O. Sidecar caching is controlled by the catalog option
+`cache.manifest-sidecar.max-memory` (64 MiB by default). A positive value supplies an
+additional budget independent of the manifest content cache. When set to 0, sidecars reuse
+the manifest content cache, or remain uncached if that cache is disabled. Sidecar caching
+uses the catalog's `cache.expire-after-access` and `cache.manifest.soft-values` policies.
+Selected block bytes still share the manifest content cache without populating the
+whole-manifest entry cache with partial results. The low-level `build` method returns
+sidecar bytes without writing or publishing another file.
Callers decide whether to invoke `build` and `read`; these utilities have no read/write switches.
`build` and `Builder` accept `rowIdEnabled` and `bucketEnabled` arguments for independent
payload generation. Partition generation is always enabled,
including the empty partition tuple for unpartitioned tables. Missing or invalid
-metadata makes only the affected block's dimension unavailable. There is no sidecar byte budget:
-construction keeps complete coverage and `read` consumes the entire file once it is opened.
+metadata makes only the affected block's dimension unavailable. Cache limits do not truncate
+sidecars: construction keeps complete coverage and `read` consumes the entire file once it is opened.
`read` returns null for an absent sidecar reference or an `IOException`, allowing the caller
to fall back to the manifest. If the thread is interrupted, the I/O failure is propagated as
diff --git a/docs/generated/catalog_configuration.html b/docs/generated/catalog_configuration.html
index 806edac3f870..bc7226d1b07d 100644
--- a/docs/generated/catalog_configuration.html
+++ b/docs/generated/catalog_configuration.html
@@ -50,6 +50,12 @@
Duration
Cache expiration policy: marks cache entries to expire after a specified duration has passed since their last refresh.
+
+ cache.manifest-sidecar.max-memory
+ 64 mb
+ MemorySize
+ Controls the maximum memory for the separate manifest sidecar cache. This budget is additional to the manifest content cache. Set to 0 to reuse the manifest content cache. It uses 'cache.expire-after-access' and 'cache.manifest.soft-values'.
+
cache.manifest.max-memory
(none)
diff --git a/paimon-api/src/main/java/org/apache/paimon/options/CatalogOptions.java b/paimon-api/src/main/java/org/apache/paimon/options/CatalogOptions.java
index b1ccfe1caada..bda512a944b8 100644
--- a/paimon-api/src/main/java/org/apache/paimon/options/CatalogOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/options/CatalogOptions.java
@@ -130,6 +130,16 @@ public class CatalogOptions {
.noDefaultValue()
.withDescription("Controls the maximum memory to cache manifest content.");
+ public static final ConfigOption CACHE_MANIFEST_SIDECAR_MAX_MEMORY =
+ key("cache.manifest-sidecar.max-memory")
+ .memoryType()
+ .defaultValue(MemorySize.ofMebiBytes(64))
+ .withDescription(
+ "Controls the maximum memory for the separate manifest sidecar cache. "
+ + "This budget is additional to the manifest content cache. "
+ + "Set to 0 to reuse the manifest content cache. It uses "
+ + "'cache.expire-after-access' and 'cache.manifest.soft-values'.");
+
public static final ConfigOption CACHE_MANIFEST_SOFT_VALUES =
key("cache.manifest.soft-values")
.booleanType()
diff --git a/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/ManifestFileSorterBenchmark.java b/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/ManifestFileSorterBenchmark.java
index e805efb69443..1f78ff8d4591 100644
--- a/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/ManifestFileSorterBenchmark.java
+++ b/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/ManifestFileSorterBenchmark.java
@@ -301,6 +301,7 @@ private ManifestFile createManifestFile() {
pathFactory,
TARGET_MANIFEST_SIZE,
null,
+ null,
new CoreOptions(new Options()))
.create();
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java b/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
index 29c7ab8de037..86839355a718 100644
--- a/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
+++ b/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
@@ -109,6 +109,7 @@ abstract class AbstractFileStore implements FileStore {
protected final CatalogEnvironment catalogEnvironment;
@Nullable private SegmentsCache readManifestCache;
+ @Nullable private SegmentsCache manifestSidecarCache;
@Nullable private Cache snapshotCache;
protected AbstractFileStore(
@@ -212,6 +213,7 @@ public ManifestFile.Factory manifestFileFactory() {
pathFactory(),
options.manifestTargetSize().getBytes(),
readManifestCache,
+ manifestSidecarCache,
options);
}
@@ -616,6 +618,11 @@ public void setManifestCache(SegmentsCache manifestCache) {
this.readManifestCache = manifestCache;
}
+ @Override
+ public void setManifestSidecarCache(SegmentsCache manifestSidecarCache) {
+ this.manifestSidecarCache = manifestSidecarCache;
+ }
+
@Override
public void setSnapshotCache(Cache cache) {
this.snapshotCache = cache;
diff --git a/paimon-core/src/main/java/org/apache/paimon/FileStore.java b/paimon-core/src/main/java/org/apache/paimon/FileStore.java
index 1714eec2a983..d9b143f75f59 100644
--- a/paimon-core/src/main/java/org/apache/paimon/FileStore.java
+++ b/paimon-core/src/main/java/org/apache/paimon/FileStore.java
@@ -127,5 +127,7 @@ boolean mergeSchema(
void setManifestCache(SegmentsCache manifestCache);
+ void setManifestSidecarCache(SegmentsCache manifestSidecarCache);
+
void setSnapshotCache(Cache cache);
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java
index 7fce5edf0ef4..037f7ea2bfba 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/CachingCatalog.java
@@ -51,6 +51,7 @@
import static org.apache.paimon.options.CatalogOptions.CACHE_EXPIRE_AFTER_ACCESS;
import static org.apache.paimon.options.CatalogOptions.CACHE_EXPIRE_AFTER_WRITE;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_MAX_MEMORY;
+import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SIDECAR_MAX_MEMORY;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SMALL_FILE_MEMORY;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SMALL_FILE_THRESHOLD;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SOFT_VALUES;
@@ -71,6 +72,7 @@ public class CachingCatalog extends DelegateCatalog {
protected Cache databaseCache;
protected Cache tableCache;
@Nullable protected final SegmentsCache manifestCache;
+ @Nullable protected final SegmentsCache manifestSidecarCache;
// partition cache will affect data latency
@Nullable protected Cache> partitionCache;
@Nullable protected DVMetaCache dvMetaCache;
@@ -108,6 +110,17 @@ public CachingCatalog(Catalog wrapped, Options options) {
expireAfterAccess,
manifestCacheSoftValues);
+ MemorySize sidecarMaxMemory = options.get(CACHE_MANIFEST_SIDECAR_MAX_MEMORY);
+ this.manifestSidecarCache =
+ sidecarMaxMemory.getBytes() == 0
+ ? manifestCache
+ : SegmentsCache.create(
+ (int) CoreOptions.PAGE_SIZE.defaultValue().getBytes(),
+ sidecarMaxMemory,
+ sidecarMaxMemory.getBytes(),
+ expireAfterAccess,
+ manifestCacheSoftValues);
+
this.cachedPartitionMaxNum = options.get(CACHE_PARTITION_MAX_NUM);
int cacheDvMaxNum = options.get(CACHE_DV_MAX_NUM);
@@ -299,6 +312,9 @@ private Table loadTable(Identifier identifier) {
if (manifestCache != null) {
storeTable.setManifestCache(manifestCache);
}
+ if (manifestSidecarCache != null) {
+ storeTable.setManifestSidecarCache(manifestSidecarCache);
+ }
if (dvMetaCache != null) {
storeTable.setDVMetaCache(dvMetaCache);
}
@@ -425,6 +441,11 @@ public CacheSizes estimatedCacheSizes() {
manifestCacheSize = manifestCache.estimatedSize();
manifestCacheBytes = manifestCache.totalCacheBytes();
}
+ // Keep reporting total manifest metadata usage even though sidecars have their own budget.
+ if (manifestSidecarCache != null && manifestSidecarCache != manifestCache) {
+ manifestCacheSize += manifestSidecarCache.estimatedSize();
+ manifestCacheBytes += manifestSidecarCache.totalCacheBytes();
+ }
long partitionCacheSize = 0L;
if (partitionCache != null) {
for (Map.Entry> entry : partitionCache.asMap().entrySet()) {
diff --git a/paimon-core/src/main/java/org/apache/paimon/manifest/BucketFilter.java b/paimon-core/src/main/java/org/apache/paimon/manifest/BucketFilter.java
index 536be456eef0..3e564a60f9e1 100644
--- a/paimon-core/src/main/java/org/apache/paimon/manifest/BucketFilter.java
+++ b/paimon-core/src/main/java/org/apache/paimon/manifest/BucketFilter.java
@@ -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();
diff --git a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java
index 30e0adcf1bf3..9740961e73d2 100644
--- a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java
+++ b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java
@@ -37,6 +37,7 @@
import org.apache.paimon.utils.Filter;
import org.apache.paimon.utils.ObjectsFile;
import org.apache.paimon.utils.PathFactory;
+import org.apache.paimon.utils.RowRangeIndex;
import org.apache.paimon.utils.SegmentsCache;
import javax.annotation.Nullable;
@@ -61,6 +62,7 @@ public class ManifestFile extends ObjectsFile {
private final AvroFileFormat avroFileFormat;
private final long suggestedFileSize;
private final CoreOptions options;
+ @Nullable private final SegmentsCache sidecarCache;
private ManifestFile(
FileIO fileIO,
@@ -72,6 +74,7 @@ private ManifestFile(
PathFactory pathFactory,
long suggestedFileSize,
@Nullable SegmentsCache cache,
+ @Nullable SegmentsCache sidecarCache,
CoreOptions options) {
super(
fileIO,
@@ -89,6 +92,7 @@ private ManifestFile(
this.avroFileFormat = avroFileFormat;
this.suggestedFileSize = suggestedFileSize;
this.options = options;
+ this.sidecarCache = sidecarCache == null ? cache : sidecarCache;
}
@Override
@@ -140,9 +144,33 @@ public List read(
Filter readFilter,
Filter readTFilter,
Function convertor) {
+ return read(
+ fileName,
+ fileSize,
+ partitionFilter,
+ bucketFilter,
+ readFilter,
+ readTFilter,
+ convertor,
+ null);
+ }
+
+ public List read(
+ String fileName,
+ @Nullable Long fileSize,
+ @Nullable PartitionPredicate partitionFilter,
+ @Nullable BucketFilter bucketFilter,
+ Filter readFilter,
+ Filter readTFilter,
+ Function convertor,
+ @Nullable ManifestSidecar.Selection selected) {
+ if (selected != null && selected.blocks().isEmpty()) {
+ return java.util.Collections.emptyList();
+ }
try {
Path path = pathFactory.toPath(fileName);
- if (cache != null) {
+ // Sidecar selections use the block cache, even when every block is selected.
+ if (cache != null && selected == null) {
ManifestEntryFilters filters =
new ManifestEntryFilters(
partitionFilter, bucketFilter, readFilter, readTFilter);
@@ -155,7 +183,9 @@ public List read(
path,
ManifestEntry.MANIFEST_ROW_TYPE,
partitionFilter,
- bucketFilter);
+ bucketFilter,
+ selected,
+ cache == null ? null : cache.segmentsCache());
return readFromIterator(iterator, serializer, readFilter, readTFilter, convertor);
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -209,8 +239,23 @@ private static CloseableIterator createManifestIterator(
@Nullable PartitionPredicate partitionFilter,
@Nullable BucketFilter bucketFilter)
throws IOException {
+ return createManifestIterator(
+ fileIO, path, projectedType, partitionFilter, bucketFilter, null, null);
+ }
+
+ private static CloseableIterator createManifestIterator(
+ FileIO fileIO,
+ Path path,
+ RowType projectedType,
+ @Nullable PartitionPredicate partitionFilter,
+ @Nullable BucketFilter bucketFilter,
+ @Nullable ManifestSidecar.Selection selected,
+ @Nullable SegmentsCache cache)
+ throws IOException {
try {
- ManifestAvroReader reader = new ManifestAvroReader(fileIO.newInputStream(path));
+ ManifestAvroReader reader =
+ new ManifestAvroReader(
+ ManifestSidecar.openManifest(fileIO, path, selected, cache));
return reader.read(projectedType, partitionFilter, bucketFilter);
} catch (IOException e) {
FileUtils.checkExists(fileIO, path);
@@ -345,6 +390,32 @@ public Path toPath(String fileName) {
};
}
+ @Nullable
+ public ManifestSidecar.Selection selectBlocks(
+ ManifestFileMeta manifest, @Nullable RowRangeIndex query) {
+ return selectBlocks(manifest, query, null, null);
+ }
+
+ @Nullable
+ public ManifestSidecar.Selection selectBlocks(
+ ManifestFileMeta manifest,
+ @Nullable RowRangeIndex query,
+ @Nullable PartitionPredicate partitionFilter,
+ @Nullable BucketFilter bucketFilter) {
+ return !options.manifestSidecarEnabled()
+ || (query == null && partitionFilter == null && bucketFilter == null)
+ ? null
+ : ManifestSidecar.read(
+ fileIO,
+ pathFactory.toPath(manifest.fileName()),
+ manifest,
+ query,
+ partitionFilter,
+ partitionType,
+ bucketFilter == null ? null : bucketFilter::mayContain,
+ sidecarCache);
+ }
+
/** Deletes an unreferenced manifest and its explicitly referenced extra files. */
public void delete(ManifestFileMeta manifest) {
delete(manifest.fileName());
@@ -365,6 +436,7 @@ public static class Factory {
private final long suggestedFileSize;
private final CoreOptions options;
@Nullable private final SegmentsCache cache;
+ @Nullable private final SegmentsCache sidecarCache;
public Factory(
FileIO fileIO,
@@ -375,6 +447,7 @@ public Factory(
FileStorePathFactory pathFactory,
long suggestedFileSize,
@Nullable SegmentsCache cache,
+ @Nullable SegmentsCache sidecarCache,
CoreOptions options) {
this.fileIO = fileIO;
this.schemaManager = schemaManager;
@@ -384,13 +457,10 @@ public Factory(
this.pathFactory = pathFactory;
this.suggestedFileSize = suggestedFileSize;
this.cache = cache;
+ this.sidecarCache = sidecarCache;
this.options = options;
}
- public boolean isCacheEnabled() {
- return cache != null;
- }
-
public ManifestFile create() {
return new ManifestFile(
fileIO,
@@ -402,6 +472,7 @@ public ManifestFile create() {
pathFactory.manifestFileFactory(),
suggestedFileSize,
cache,
+ sidecarCache,
options);
}
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestSidecar.java b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestSidecar.java
index c6ee9e9f5615..6a8ce2b143bb 100644
--- a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestSidecar.java
+++ b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestSidecar.java
@@ -632,7 +632,7 @@ public static Selection read(
@Nullable PartitionPredicate partitionFilter,
@Nullable RowType partitionType,
@Nullable BiPredicate bucketFilter,
- @Nullable SegmentsCache cache) {
+ @Nullable SegmentsCache cache) {
String sidecarFileName = fileName(manifest);
if (sidecarFileName == null) {
return null;
@@ -660,7 +660,7 @@ public static Selection read(
}
}
- /** Complete sidecar bytes stored in the shared manifest cache. */
+ /** Complete sidecar bytes stored in the configured sidecar cache. */
static final class ManifestSidecarSegment implements Segments {
private final byte[] bytes;
diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreScan.java b/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreScan.java
index 82923ef9d827..e40d27e789b8 100644
--- a/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreScan.java
+++ b/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreScan.java
@@ -29,6 +29,7 @@
import org.apache.paimon.manifest.ManifestEntrySerializer;
import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.manifest.ManifestSidecar;
import org.apache.paimon.manifest.PartitionEntry;
import org.apache.paimon.manifest.SimpleFileEntry;
import org.apache.paimon.operation.metrics.ScanMetrics;
@@ -498,27 +499,35 @@ private List readManifest(
@Nullable Filter additionalFilter,
@Nullable Filter additionalTFilter) {
+ ManifestFile manifestFile = manifestFileFactory.create();
+ BucketFilter bucketFilter = createBucketFilter();
+ ManifestSidecar.Selection selected =
+ manifestFile.selectBlocks(
+ manifest, rowRangeIndex, manifestsReader.partitionFilter(), bucketFilter);
+ if (selected != null && selected.blocks().isEmpty()) {
+ return Collections.emptyList();
+ }
Filter entryRowFilter = createEntryRowFilter();
Function finalConverter =
dropStats ? e -> converter.apply(dropStats(e)) : converter;
List entries =
- manifestFileFactory
- .create()
+ manifestFile
.withCacheMetrics(
scanMetrics != null ? scanMetrics.getCacheMetrics() : null)
.read(
manifest.fileName(),
manifest.fileSize(),
manifestsReader.partitionFilter(),
- createBucketFilter(),
+ bucketFilter,
entryRowFilter.and(additionalFilter),
entry ->
(additionalTFilter == null || additionalTFilter.test(entry))
&& (manifestEntryFilter == null
|| manifestEntryFilter.test(entry))
&& filterByStats(entry),
- finalConverter);
+ finalConverter,
+ selected);
LOG.info("Read {} manifest entries from {}", entries.size(), manifest.fileName());
return entries;
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java b/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
index e9d9603f5a98..93ecc173d5d2 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java
@@ -107,6 +107,7 @@ abstract class AbstractFileStoreTable implements FileStoreTable {
@Nullable private Set appliedDynamicOptionKeys;
@Nullable protected transient SegmentsCache manifestCache;
+ @Nullable protected transient SegmentsCache manifestSidecarCache;
@Nullable protected transient Cache snapshotCache;
@Nullable protected transient Cache statsCache;
@Nullable protected transient DVMetaCache dvmetaCache;
@@ -144,6 +145,18 @@ public SegmentsCache getManifestCache() {
return manifestCache;
}
+ @Override
+ public void setManifestSidecarCache(SegmentsCache manifestSidecarCache) {
+ this.manifestSidecarCache = manifestSidecarCache;
+ store().setManifestSidecarCache(manifestSidecarCache);
+ }
+
+ @Nullable
+ @Override
+ public SegmentsCache getManifestSidecarCache() {
+ return manifestSidecarCache;
+ }
+
@Override
public void setSnapshotCache(Cache cache) {
this.snapshotCache = cache;
@@ -432,6 +445,9 @@ public FileStoreTable copy(TableSchema newTableSchema) {
if (manifestCache != null) {
copied.setManifestCache(manifestCache);
}
+ if (manifestSidecarCache != null) {
+ copied.setManifestSidecarCache(manifestSidecarCache);
+ }
if (statsCache != null) {
copied.setStatsCache(statsCache);
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java b/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
index 9a5e81bcb636..ed541721fca0 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/DelegatedFileStoreTable.java
@@ -145,6 +145,17 @@ public SegmentsCache getManifestCache() {
return wrapped.getManifestCache();
}
+ @Override
+ public void setManifestSidecarCache(SegmentsCache manifestSidecarCache) {
+ wrapped.setManifestSidecarCache(manifestSidecarCache);
+ }
+
+ @Nullable
+ @Override
+ public SegmentsCache getManifestSidecarCache() {
+ return wrapped.getManifestSidecarCache();
+ }
+
@Override
public void setSnapshotCache(Cache cache) {
wrapped.setSnapshotCache(cache);
diff --git a/paimon-core/src/main/java/org/apache/paimon/table/FallbackReadFileStoreTable.java b/paimon-core/src/main/java/org/apache/paimon/table/FallbackReadFileStoreTable.java
index da37e0780825..940dd38b2b63 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/FallbackReadFileStoreTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/FallbackReadFileStoreTable.java
@@ -149,6 +149,12 @@ public void setManifestCache(SegmentsCache manifestCache) {
other.setManifestCache(manifestCache);
}
+ @Override
+ public void setManifestSidecarCache(SegmentsCache manifestSidecarCache) {
+ super.setManifestSidecarCache(manifestSidecarCache);
+ other.setManifestSidecarCache(manifestSidecarCache);
+ }
+
protected FileStoreTable switchWrappedToBranch(String branchName) {
Optional optionalSchema =
wrapped.schemaManager().copyWithBranch(branchName).latest();
diff --git a/paimon-core/src/main/java/org/apache/paimon/table/FileStoreTable.java b/paimon-core/src/main/java/org/apache/paimon/table/FileStoreTable.java
index 970493430589..ddaa17407624 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/FileStoreTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/FileStoreTable.java
@@ -70,6 +70,11 @@ interface SnapshotReaderFactory {
@Nullable
SegmentsCache getManifestCache();
+ void setManifestSidecarCache(SegmentsCache manifestSidecarCache);
+
+ @Nullable
+ SegmentsCache getManifestSidecarCache();
+
void setSnapshotCache(Cache cache);
void setStatsCache(Cache cache);
diff --git a/paimon-core/src/main/java/org/apache/paimon/utils/ObjectsCache.java b/paimon-core/src/main/java/org/apache/paimon/utils/ObjectsCache.java
index 4530f698e291..59106c5a2a70 100644
--- a/paimon-core/src/main/java/org/apache/paimon/utils/ObjectsCache.java
+++ b/paimon-core/src/main/java/org/apache/paimon/utils/ObjectsCache.java
@@ -63,6 +63,12 @@ public void withCacheMetrics(@Nullable CacheMetrics cacheMetrics) {
this.cacheMetrics = cacheMetrics;
}
+ /** Shares the byte cache with consumers using distinct whole-file and block keys. */
+ @SuppressWarnings("unchecked")
+ public SegmentsCache segmentsCache() {
+ return (SegmentsCache) (SegmentsCache>) cache;
+ }
+
public List read(K key, @Nullable Long fileSize, Filters filters) throws IOException {
return read(key, fileSize, filters, Function.identity());
}
diff --git a/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java
index 3de7bbf73f16..71427641b79f 100644
--- a/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/catalog/CachingCatalogTest.java
@@ -22,12 +22,17 @@
import org.apache.paimon.Snapshot;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.fs.Path;
+import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.manifest.ManifestSidecar;
import org.apache.paimon.options.MemorySize;
import org.apache.paimon.options.Options;
import org.apache.paimon.partition.Partition;
+import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.Table;
import org.apache.paimon.table.sink.BatchTableCommit;
import org.apache.paimon.table.sink.BatchTableWrite;
@@ -48,6 +53,8 @@
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.io.FileNotFoundException;
@@ -71,6 +78,7 @@
import static org.apache.paimon.data.BinaryString.fromString;
import static org.apache.paimon.options.CatalogOptions.CACHE_EXPIRE_AFTER_ACCESS;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_MAX_MEMORY;
+import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SIDECAR_MAX_MEMORY;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SMALL_FILE_MEMORY;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SMALL_FILE_THRESHOLD;
import static org.apache.paimon.options.CatalogOptions.CACHE_MANIFEST_SOFT_VALUES;
@@ -633,4 +641,138 @@ public void testManifestCacheOptions() {
assertThat(caching.manifestCache.softValues()).isFalse();
assertThat(caching.manifestCache.ttl()).isEqualTo(CACHE_EXPIRE_AFTER_ACCESS.defaultValue());
}
+
+ @Test
+ public void testManifestSidecarCacheOptions() {
+ Options options = new Options();
+ CachingCatalog caching = new CachingCatalog(catalog, options);
+ assertThat(CACHE_MANIFEST_SIDECAR_MAX_MEMORY.defaultValue())
+ .isEqualTo(MemorySize.ofMebiBytes(64));
+ assertThat(caching.manifestSidecarCache).isNotSameAs(caching.manifestCache);
+ assertThat(caching.manifestSidecarCache.maxMemorySize())
+ .isEqualTo(MemorySize.ofMebiBytes(64));
+ assertThat(caching.manifestSidecarCache.maxElementSize())
+ .isEqualTo(MemorySize.ofMebiBytes(64).getBytes());
+
+ options.set(CACHE_MANIFEST_SIDECAR_MAX_MEMORY, MemorySize.ofMebiBytes(8));
+ caching = new CachingCatalog(catalog, options);
+ assertThat(caching.manifestSidecarCache).isNotSameAs(caching.manifestCache);
+ assertThat(caching.manifestSidecarCache.maxMemorySize())
+ .isEqualTo(MemorySize.ofMebiBytes(8));
+ assertThat(caching.manifestSidecarCache.maxElementSize())
+ .isEqualTo(MemorySize.ofMebiBytes(8).getBytes());
+ assertThat(caching.manifestSidecarCache.ttl())
+ .isEqualTo(CACHE_EXPIRE_AFTER_ACCESS.defaultValue());
+ assertThat(caching.manifestSidecarCache.softValues()).isTrue();
+
+ options.set(CACHE_MANIFEST_SMALL_FILE_MEMORY, MemorySize.ofBytes(0));
+ options.set(CACHE_EXPIRE_AFTER_ACCESS, Duration.ofMinutes(2));
+ options.set(CACHE_MANIFEST_SOFT_VALUES, false);
+ caching = new CachingCatalog(catalog, options);
+ assertThat(caching.manifestCache).isNull();
+ assertThat(caching.manifestSidecarCache.maxMemorySize())
+ .isEqualTo(MemorySize.ofMebiBytes(8));
+ assertThat(caching.manifestSidecarCache.ttl()).isEqualTo(Duration.ofMinutes(2));
+ assertThat(caching.manifestSidecarCache.softValues()).isFalse();
+
+ options.set(CACHE_MANIFEST_SIDECAR_MAX_MEMORY, MemorySize.ofBytes(0));
+ options.set(CACHE_MANIFEST_SMALL_FILE_MEMORY, MemorySize.ofMebiBytes(1));
+ caching = new CachingCatalog(catalog, options);
+ assertThat(caching.manifestCache).isNotNull();
+ assertThat(caching.manifestSidecarCache).isSameAs(caching.manifestCache);
+
+ options.set(CACHE_MANIFEST_SMALL_FILE_MEMORY, MemorySize.ofBytes(0));
+ caching = new CachingCatalog(catalog, options);
+ assertThat(caching.manifestCache).isNull();
+ assertThat(caching.manifestSidecarCache).isNull();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testManifestSidecarCacheSharedAcrossTableCopies(boolean separate) throws Exception {
+ Options options = new Options();
+ options.set(CACHE_MANIFEST_SOFT_VALUES, false);
+ options.set(
+ CACHE_MANIFEST_SIDECAR_MAX_MEMORY,
+ separate ? MemorySize.ofMebiBytes(1) : MemorySize.ofBytes(0));
+ CachingCatalog caching = new CachingCatalog(catalog, options);
+ Identifier id = Identifier.create("db", "sidecar_cache");
+ caching.createTable(id, DEFAULT_TABLE_SCHEMA, false);
+ caching.alterTable(
+ id,
+ SchemaChange.setOption(CoreOptions.MANIFEST_SIDECAR_ENABLED.key(), "true"),
+ false);
+ FileStoreTable table = (FileStoreTable) caching.getTable(id);
+ assertThat(table.getManifestSidecarCache()).isSameAs(caching.manifestSidecarCache);
+ writeTableForTestManifestCache(table);
+ ManifestFileMeta meta =
+ table.store()
+ .manifestListFactory()
+ .create()
+ .readDataManifests(table.latestSnapshot().get())
+ .get(0);
+ PartitionPredicate partition = PartitionPredicate.ALWAYS_TRUE;
+ assertThat(
+ table.store()
+ .manifestFileFactory()
+ .create()
+ .selectBlocks(meta, null, partition, null))
+ .isNotNull();
+ Path sidecar = new Path(table.location(), "manifest/" + ManifestSidecar.fileName(meta));
+ assertThat(caching.manifestSidecarCache.getIfPresents(sidecar)).isNotNull();
+ if (separate) {
+ assertThat(caching.manifestCache.getIfPresents(sidecar)).isNull();
+ } else {
+ assertThat(caching.manifestCache.getIfPresents(sidecar)).isNotNull();
+ }
+ assertThat(caching.estimatedCacheSizes().manifestCacheSize())
+ .isEqualTo(
+ caching.manifestCache.estimatedSize()
+ + (separate ? caching.manifestSidecarCache.estimatedSize() : 0));
+ assertThat(caching.estimatedCacheSizes().manifestCacheBytes())
+ .isEqualTo(
+ caching.manifestCache.totalCacheBytes()
+ + (separate ? caching.manifestSidecarCache.totalCacheBytes() : 0));
+
+ // A copied table and a new ManifestFile must reuse cached bytes even after the file is
+ // gone.
+ fileIO.delete(sidecar, false);
+ for (FileStoreTable copy :
+ new FileStoreTable[] {
+ table.copy(singletonMap("a", "b")),
+ table.copy(table.schema()),
+ (FileStoreTable) caching.getTable(id)
+ }) {
+ assertThat(copy.getManifestSidecarCache()).isSameAs(caching.manifestSidecarCache);
+ assertThat(
+ copy.store()
+ .manifestFileFactory()
+ .create()
+ .selectBlocks(meta, null, partition, null))
+ .isNotNull();
+ }
+ }
+
+ @Test
+ public void testManifestSidecarCachePropagatesToFallbackTable() throws Exception {
+ CachingCatalog caching = new CachingCatalog(catalog, new Options());
+ Identifier id = Identifier.create("db", "sidecar_fallback");
+ caching.createTable(id, DEFAULT_TABLE_SCHEMA, false);
+ caching.getTable(id).createBranch("fallback");
+ caching.alterTable(
+ id,
+ SchemaChange.setOption(CoreOptions.SCAN_FALLBACK_BRANCH.key(), "fallback"),
+ false);
+ FallbackReadFileStoreTable table = (FallbackReadFileStoreTable) caching.getTable(id);
+ for (FallbackReadFileStoreTable copy :
+ new FallbackReadFileStoreTable[] {
+ table, (FallbackReadFileStoreTable) table.copy(singletonMap("a", "b"))
+ }) {
+ assertThat(copy.getManifestSidecarCache()).isSameAs(caching.manifestSidecarCache);
+ assertThat(copy.wrapped().getManifestSidecarCache())
+ .isSameAs(caching.manifestSidecarCache);
+ assertThat(copy.other().getManifestSidecarCache())
+ .isSameAs(caching.manifestSidecarCache);
+ }
+ }
}
diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
index 16f33f4cee97..161591a48e11 100644
--- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTest.java
@@ -2745,6 +2745,7 @@ public void testManifestSortWithMultiplePartitions() {
null),
Long.MAX_VALUE,
null,
+ null,
new CoreOptions(new Options()))
.create();
@@ -3206,6 +3207,7 @@ private ManifestFile createManifestFileForPartitionType(RowType partitionType) {
null),
Long.MAX_VALUE,
null,
+ null,
new CoreOptions(new Options()))
.create();
}
diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTestBase.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTestBase.java
index a4516015c29b..4e72f58e7382 100644
--- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTestBase.java
+++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileMetaTestBase.java
@@ -167,6 +167,7 @@ protected ManifestFile createManifestFile(String pathStr, FileIO fileIO) {
null),
Long.MAX_VALUE,
null,
+ null,
new CoreOptions(new Options()))
.create();
}
diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java
index 0362871ef826..db914239eea5 100644
--- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java
@@ -19,6 +19,7 @@
package org.apache.paimon.manifest;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.format.FileFormat;
@@ -28,13 +29,18 @@
import org.apache.paimon.fs.FileIOFinder;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.fs.SeekableInputStream;
+import org.apache.paimon.fs.SeekableInputStreamWrapper;
import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.io.DataFileMeta;
import org.apache.paimon.io.DataFileMetaWriteColsLegacySerializer;
+import org.apache.paimon.operation.AppendOnlyFileStoreScan;
+import org.apache.paimon.operation.ManifestsReader;
import org.apache.paimon.options.MemorySize;
import org.apache.paimon.options.Options;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.schema.FileSystemSchemaManager;
+import org.apache.paimon.schema.TableSchema;
import org.apache.paimon.stats.StatsTestUtils;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataType;
@@ -44,6 +50,8 @@
import org.apache.paimon.utils.FailingFileIO;
import org.apache.paimon.utils.FileStorePathFactory;
import org.apache.paimon.utils.Filter;
+import org.apache.paimon.utils.Range;
+import org.apache.paimon.utils.RowRangeIndex;
import org.apache.paimon.utils.SegmentsCache;
import org.junit.jupiter.api.RepeatedTest;
@@ -68,13 +76,18 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.paimon.TestKeyValueGenerator.DEFAULT_PART_TYPE;
+import static org.apache.paimon.manifest.ManifestIndexTestUtils.withExtraFiles;
import static org.apache.paimon.stats.StatsTestUtils.convertWithoutSchemaEvolution;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
/** Tests for {@link ManifestFile}. */
public class ManifestFileTest {
@@ -1405,16 +1418,646 @@ private static int indexOf(byte[] bytes, byte[] target, int from, int limit) {
return -1;
}
+ @Test
+ void testSidecarCacheUsesExplicitPathsAndIsSeparateFromManifestCache() throws Exception {
+ Options options = new Options();
+ options.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO io = new RecordingFileIO();
+ SegmentsCache cache =
+ new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), Long.MAX_VALUE, null, false);
+ SegmentsCache sidecarCache =
+ new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), Long.MAX_VALUE, null, false);
+ ManifestFile.Factory factory =
+ createManifestFileFactory(
+ tempDir.toString(), Long.MAX_VALUE, options, io, cache, sidecarCache);
+ List entries = new ArrayList<>();
+ for (int i = 0; i < 2; i++) {
+ ManifestEntry entry = gen.next();
+ entries.add(
+ ManifestEntry.create(
+ FileKind.ADD,
+ entry.partition(),
+ entry.bucket(),
+ entry.totalBuckets(),
+ entry.file().newFirstRowId(i * 1000000L)));
+ }
+ ManifestFileMeta written = factory.create().write(entries).get(0);
+ String sidecarName = "cached-explicit" + ManifestSidecar.SUFFIX;
+ java.nio.file.Path sidecar = tempDir.resolve("manifest").resolve(sidecarName);
+ java.nio.file.Files.move(
+ tempDir.resolve("manifest").resolve(ManifestSidecar.fileName(written)), sidecar);
+ ManifestFileMeta meta = withExtraFiles(written, Collections.singletonList(sidecarName));
+ Path sidecarPath = new Path(tempDir.toString(), "manifest/" + sidecarName);
+
+ io.reset();
+ assertThat(
+ factory.create()
+ .selectBlocks(
+ meta,
+ RowRangeIndex.create(
+ Collections.singletonList(
+ new Range(Long.MAX_VALUE, Long.MAX_VALUE))))
+ .blocks())
+ .isEmpty();
+ assertThat(io.opened).containsExactly(sidecarPath);
+ assertThat(sidecarCache.getIfPresents(sidecarPath).totalMemorySize())
+ .isEqualTo(java.nio.file.Files.size(sidecar));
+ assertThat(cache.estimatedSize()).isZero();
+
+ io.reset();
+ RowRangeIndex hit = RowRangeIndex.create(Collections.singletonList(new Range(0, 0)));
+ assertThat(factory.create().selectBlocks(meta, hit).blocks()).isNotEmpty();
+ assertThat(io.opened).isEmpty();
+ assertThat(factory.create().read(meta.fileName()))
+ .containsExactlyInAnyOrderElementsOf(entries);
+ assertThat(cache.estimatedSize()).isEqualTo(1);
+ assertThat(sidecarCache.estimatedSize()).isEqualTo(1);
+ assertThat(cache.getIfPresents(sidecarPath)).isNull();
+ assertThat(
+ sidecarCache.getIfPresents(
+ new Path(tempDir.toString(), "manifest/" + meta.fileName())))
+ .isNull();
+
+ io.reset();
+ assertThat(factory.create().selectBlocks(meta, hit).blocks()).isNotEmpty();
+ assertThat(factory.create().read(meta.fileName()))
+ .containsExactlyInAnyOrderElementsOf(entries);
+ assertThat(io.opened).isEmpty();
+ }
+
+ @Test
+ void testDedicatedSidecarCacheAndManifestCacheFallback() {
+ Options options = new Options();
+ options.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ for (boolean cacheManifest : new boolean[] {false, true}) {
+ for (boolean cacheSidecar : new boolean[] {false, true}) {
+ RecordingFileIO io = new RecordingFileIO();
+ SegmentsCache manifestCache =
+ cacheManifest
+ ? new SegmentsCache<>(
+ 1024,
+ MemorySize.ofMebiBytes(1),
+ Long.MAX_VALUE,
+ null,
+ false)
+ : null;
+ SegmentsCache sidecarCache =
+ cacheSidecar
+ ? new SegmentsCache<>(
+ 1024,
+ MemorySize.ofMebiBytes(1),
+ Long.MAX_VALUE,
+ null,
+ false)
+ : null;
+ ManifestFile.Factory factory =
+ createManifestFileFactory(
+ tempDir.resolve(cacheManifest + "-" + cacheSidecar).toString(),
+ Long.MAX_VALUE,
+ options,
+ io,
+ manifestCache,
+ sidecarCache);
+ ManifestEntry entry = gen.next();
+ ManifestEntry added =
+ ManifestEntry.create(
+ FileKind.ADD,
+ entry.partition(),
+ entry.bucket(),
+ entry.totalBuckets(),
+ entry.file().newFirstRowId(0L));
+ ManifestFileMeta meta =
+ factory.create().write(Collections.singletonList(added)).get(0);
+ RowRangeIndex query =
+ RowRangeIndex.create(Collections.singletonList(new Range(0, 0)));
+
+ for (int round = 0; round < 2; round++) {
+ io.reset();
+ ManifestFile manifest = factory.create();
+ ManifestSidecar.Selection selected = manifest.selectBlocks(meta, query);
+ assertThat(readSelectedEntries(manifest, meta, selected))
+ .containsExactly(added);
+ assertThat(
+ io.opened.stream()
+ .filter(
+ path ->
+ path.getName()
+ .endsWith(
+ ManifestSidecar
+ .SUFFIX)))
+ .hasSize(round == 0 || (!cacheSidecar && !cacheManifest) ? 1 : 0);
+ assertThat(
+ io.opened.stream()
+ .filter(path -> path.getName().equals(meta.fileName())))
+ .hasSize(round == 0 || !cacheManifest ? 1 : 0);
+ }
+ if (manifestCache != null) {
+ // Blocks stay in the manifest cache; sidecar bytes only join them on fallback.
+ assertThat(manifestCache.estimatedSize()).isEqualTo(cacheSidecar ? 1 : 2);
+ }
+ if (sidecarCache != null) {
+ assertThat(sidecarCache.estimatedSize()).isEqualTo(1);
+ }
+ }
+ }
+ }
+
+ @Test
+ void testReadsOnlySelectedBlocksAndPreservesPhysicalOrdinals() throws Exception {
+ Options options = new Options();
+ options.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO fileIO = new RecordingFileIO();
+ SegmentsCache cache =
+ new SegmentsCache<>(1024, MemorySize.ofMebiBytes(16), Long.MAX_VALUE);
+ ManifestFile.Factory factory =
+ createManifestFileFactory(
+ tempDir.toString(), Long.MAX_VALUE, options, fileIO, cache);
+ ManifestFile manifests = factory.create();
+ List entries = new ArrayList<>();
+ for (int i = 0; i < 4000; i++) {
+ ManifestEntry entry = gen.next();
+ entries.add(
+ ManifestEntry.create(
+ FileKind.ADD,
+ entry.partition(),
+ entry.bucket(),
+ entry.totalBuckets(),
+ entry.file().newFirstRowId(i * 1000000L)));
+ }
+ ManifestFileMeta meta = manifests.write(entries).get(0);
+ Path manifestPath = new Path(tempDir.toString(), "manifest/" + meta.fileName());
+ RowRangeIndex query =
+ RowRangeIndex.create(
+ Arrays.asList(
+ new Range(1000000000L, 1000000000L),
+ new Range(3000000000L, 3000000000L)));
+
+ ManifestSidecar.Selection selected = manifests.selectBlocks(meta, query);
+ assertThat(selected.blocks()).hasSize(2);
+
+ fileIO.reset();
+ List actual = readSelectedEntries(factory.create(), meta, selected);
+ List expected = new ArrayList<>();
+ for (ManifestSidecar.Block block : selected.blocks()) {
+ expected.addAll(
+ entries.subList(
+ (int) block.firstRecord,
+ (int) (block.firstRecord + block.recordCount)));
+ }
+ assertThat(actual).containsExactlyElementsOf(expected);
+ assertThat(actual).contains(entries.get(1000), entries.get(3000));
+ assertThat(fileIO.bytes.get()).isLessThan(meta.fileSize() / 4);
+ assertThat(fileIO.seeks)
+ .containsExactlyElementsOf(
+ selected.blocks().stream()
+ .map(block -> block.offset)
+ .collect(Collectors.toList()));
+ assertThat(fileIO.opened).containsExactly(manifestPath);
+ assertThat(cache.getIfPresents(manifestPath)).isNull();
+ fileIO.reset();
+ assertThat(readSelectedEntries(factory.create(), meta, selected))
+ .containsExactlyElementsOf(expected);
+ assertThat(fileIO.opened).isEmpty();
+ assertThat(fileIO.bytes.get()).isZero();
+ ManifestSidecar.Selection allBlocks =
+ manifests.selectBlocks(
+ meta,
+ RowRangeIndex.create(
+ Collections.singletonList(new Range(0, Long.MAX_VALUE))));
+ fileIO.reset();
+ assertThat(readSelectedEntries(factory.create(), meta, allBlocks))
+ .containsExactlyInAnyOrderElementsOf(entries);
+ assertThat(fileIO.opened).containsExactly(manifestPath);
+ assertThat(cache.getIfPresents(manifestPath)).isNull();
+
+ fileIO.reset();
+ assertThat(readSelectedEntries(factory.create(), meta, allBlocks))
+ .containsExactlyInAnyOrderElementsOf(entries);
+ assertThat(fileIO.opened).isEmpty();
+ assertThat(cache.getIfPresents(manifestPath)).isNull();
+
+ // Reads without a sidecar selection populate and reuse the full-manifest cache.
+ assertThat(manifests.read(meta.fileName())).containsExactlyInAnyOrderElementsOf(entries);
+ assertThat(fileIO.opened).containsExactly(manifestPath);
+ assertThat(cache.getIfPresents(manifestPath)).isNotNull();
+
+ fileIO.reset();
+ assertThat(manifests.read(meta.fileName())).containsExactlyInAnyOrderElementsOf(entries);
+ assertThat(fileIO.opened).isEmpty();
+
+ long largestBlock =
+ allBlocks.blocks().stream().mapToLong(block -> block.length).max().getAsLong();
+ assertThat(meta.fileSize()).isGreaterThan(largestBlock);
+ SegmentsCache blockCache =
+ new SegmentsCache<>(1024, MemorySize.ofMebiBytes(16), largestBlock, null, false);
+ ManifestFile.Factory limitedFactory =
+ createManifestFileFactory(
+ tempDir.toString(), Long.MAX_VALUE, options, fileIO, blockCache);
+ for (int round = 0; round < 2; round++) {
+ fileIO.reset();
+ assertThat(readSelectedEntries(limitedFactory.create(), meta, allBlocks))
+ .containsExactlyElementsOf(entries);
+ if (round == 1) {
+ assertThat(fileIO.opened).isEmpty();
+ }
+ }
+ assertThat(
+ blockCache.getIfPresents(
+ new Path(tempDir.toString(), "manifest/" + meta.fileName())))
+ .isNull();
+ }
+
+ @Test
+ void testScannerPreservesDeletesAndColumnGroups() throws Exception {
+ Options options = new Options();
+ options.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO fileIO = new RecordingFileIO();
+ ManifestFile.Factory factory =
+ createManifestFileFactory(tempDir.toString(), Long.MAX_VALUE, options, fileIO);
+ ManifestFile manifests = factory.create();
+ ManifestEntry entry = gen.next();
+ ManifestEntry add =
+ ManifestEntry.create(
+ FileKind.ADD,
+ entry.partition(),
+ entry.bucket(),
+ entry.totalBuckets(),
+ entry.file().newFirstRowId(100L));
+ ManifestEntry delete =
+ ManifestEntry.create(
+ FileKind.DELETE,
+ entry.partition(),
+ entry.bucket(),
+ entry.totalBuckets(),
+ add.file());
+ ManifestEntry other = gen.next();
+ ManifestEntry live =
+ ManifestEntry.create(
+ FileKind.ADD,
+ other.partition(),
+ other.bucket(),
+ other.totalBuckets(),
+ other.file().newFirstRowId(100L));
+ List metas = new ArrayList<>();
+ metas.addAll(manifests.write(Arrays.asList(add, live)));
+ metas.addAll(manifests.write(Collections.singletonList(delete)));
+ metas.addAll(
+ manifests.write(
+ Collections.singletonList(
+ ManifestEntry.create(
+ FileKind.ADD,
+ entry.partition(),
+ entry.bucket(),
+ entry.totalBuckets(),
+ entry.file().newFirstRowId(0L)))));
+ AppendOnlyFileStoreScan scan =
+ new AppendOnlyFileStoreScan(
+ mock(ManifestsReader.class),
+ null,
+ null,
+ null,
+ mock(TableSchema.class),
+ factory,
+ 2,
+ false,
+ false,
+ false);
+ scan.withRowRanges(Collections.singletonList(new Range(100, 100)));
+ fileIO.reset();
+ List result = new ArrayList<>();
+ scan.readManifestEntries(metas, false).forEachRemaining(result::add);
+ assertThat(result).containsExactly(live);
+ assertThat(
+ fileIO.opened.stream()
+ .filter(path -> !path.getName().endsWith(ManifestSidecar.SUFFIX))
+ .map(Path::getName))
+ .containsExactlyInAnyOrder(metas.get(0).fileName(), metas.get(1).fileName());
+
+ for (boolean missing : new boolean[] {false, true}) {
+ for (ManifestFileMeta manifest : metas) {
+ Path sidecar =
+ new Path(
+ tempDir.toString(),
+ "manifest/" + ManifestSidecar.fileName(manifest));
+ if (missing) {
+ fileIO.delete(sidecar, false);
+ } else {
+ fileIO.overwriteFileUtf8(sidecar, "corrupt sidecar");
+ }
+ }
+ List fallback = new ArrayList<>();
+ scan.readManifestEntries(metas, false).forEachRemaining(fallback::add);
+ assertThat(fallback).containsExactly(live);
+ }
+ }
+
+ @Test
+ void testBucketOnlyPlanningAndRawRewriteUseNullableBucketPayload() throws Exception {
+ Options options = new Options();
+ options.set(CoreOptions.BUCKET, 4);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO io = new RecordingFileIO();
+ ManifestFile.Factory factory =
+ createManifestFileFactory(tempDir.toString(), Long.MAX_VALUE, options, io);
+ ManifestFile manifests = factory.create();
+ List entries = new ArrayList<>();
+ for (int i = 0; i < 4000; i++) {
+ ManifestEntry entry = gen.next();
+ entries.add(
+ ManifestEntry.create(
+ FileKind.ADD, entry.partition(), i / 1000, 4, entry.file()));
+ }
+ ManifestFileMeta meta = manifests.write(entries).get(0);
+ AppendOnlyFileStoreScan scan =
+ new AppendOnlyFileStoreScan(
+ mock(ManifestsReader.class),
+ null,
+ null,
+ null,
+ mock(TableSchema.class),
+ factory,
+ 2,
+ false,
+ false,
+ false);
+ scan.withBucket(1);
+ io.reset();
+ assertThat(scan.readManifest(meta)).containsExactlyElementsOf(entries.subList(1000, 2000));
+ assertThat(io.bytes.get()).isLessThan(meta.fileSize());
+ assertThat(io.seeks).isNotEmpty();
+ ManifestAvroWriter writer = manifests.createAvroWriter();
+ try (ManifestAvroReader reader =
+ manifests.scanAvroBlocks(meta.fileName(), meta.fileSize())) {
+ writer.writeEncodedManifest(reader, meta);
+ }
+ writer.close();
+ assertThat(scan.readManifest(writer.result().get(0)))
+ .containsExactlyElementsOf(entries.subList(1000, 2000));
+
+ ManifestEntry added = entries.get(1000);
+ ManifestEntry deleted =
+ ManifestEntry.create(FileKind.DELETE, added.partition(), 1, 4, added.file());
+ List changes = new ArrayList<>();
+ changes.addAll(manifests.write(Collections.singletonList(added)));
+ changes.addAll(manifests.write(Collections.singletonList(deleted)));
+ assertThat(scan.readManifestEntries(changes, false)).isExhausted();
+ }
+
+ @Test
+ void testPartitionOnlyPlanningUsesBlocksWithoutRowIds() {
+ Options options = new Options();
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO io = new RecordingFileIO();
+ ManifestFile.Factory factory =
+ createManifestFileFactory(tempDir.toString(), Long.MAX_VALUE, options, io);
+ BinaryRow first = gen.next().partition();
+ BinaryRow second = gen.next().partition();
+ while (second.equals(first)) {
+ second = gen.next().partition();
+ }
+ List entries = new ArrayList<>();
+ for (int i = 0; i < 4000; i++) {
+ ManifestEntry entry = gen.next();
+ entries.add(
+ ManifestEntry.create(
+ FileKind.ADD,
+ i < 1000 ? first : second,
+ entry.bucket(),
+ entry.totalBuckets(),
+ entry.file()));
+ }
+ ManifestFileMeta meta = factory.create().write(entries).get(0);
+ ManifestsReader lists = mock(ManifestsReader.class);
+ when(lists.partitionFilter())
+ .thenReturn(
+ PartitionPredicate.fromMultiple(
+ DEFAULT_PART_TYPE, Collections.singletonList(first)));
+ AppendOnlyFileStoreScan scan =
+ new AppendOnlyFileStoreScan(
+ lists,
+ null,
+ null,
+ null,
+ mock(TableSchema.class),
+ factory,
+ 2,
+ false,
+ false,
+ false);
+ io.reset();
+ List actual = scan.readManifest(meta);
+ assertThat(actual).containsExactlyElementsOf(entries.subList(0, 1000));
+ assertThat(io.bytes.get()).isLessThan(meta.fileSize());
+ assertThat(io.opened).hasSize(2);
+ }
+
+ @Test
+ void testUnknownRowIdKeepsPartitionIndexAndNoQueryDoesNotReadSidecar() {
+ Options options = new Options();
+ options.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO fileIO = new RecordingFileIO();
+ ManifestFile manifests =
+ createManifestFileFactory(tempDir.toString(), Long.MAX_VALUE, options, fileIO)
+ .create();
+ ManifestFileMeta meta = manifests.write(Collections.singletonList(gen.next())).get(0);
+ assertThat(ManifestSidecar.fileName(meta)).isNotNull();
+ assertThat(
+ java.nio.file.Files.exists(
+ tempDir.resolve("manifest")
+ .resolve(meta.fileName() + ManifestSidecar.SUFFIX)))
+ .isTrue();
+
+ fileIO.reset();
+ assertThat(manifests.selectBlocks(meta, null)).isNull();
+ assertThat(fileIO.opened).isEmpty();
+ }
+
+ @Test
+ void testExplicitIndexReferenceAndNullDoesNotProbe() throws Exception {
+ Options options = new Options();
+ options.set(CoreOptions.DATA_EVOLUTION_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, true);
+ RecordingFileIO io = new RecordingFileIO();
+ ManifestFile manifests =
+ createManifestFileFactory(tempDir.toString(), Long.MAX_VALUE, options, io).create();
+ ManifestEntry original = gen.next();
+ ManifestEntry entry =
+ ManifestEntry.create(
+ FileKind.ADD,
+ original.partition(),
+ original.bucket(),
+ original.totalBuckets(),
+ original.file().newFirstRowId(100L));
+ ManifestFileMeta written = manifests.write(Collections.singletonList(entry)).get(0);
+ assertThat(ManifestSidecar.fileName(written)).isNotNull();
+ RowRangeIndex query = RowRangeIndex.create(Collections.singletonList(new Range(0, 0)));
+ io.reset();
+ for (List extraFiles :
+ Arrays.asList(
+ null,
+ Collections.emptyList(),
+ Collections.singletonList("other-partition-index"))) {
+ ManifestFileMeta unindexed = withExtraFiles(written, extraFiles);
+ assertThat(manifests.selectBlocks(unindexed, query)).isNull();
+ assertThat(io.opened).isEmpty();
+ }
+ // An existing suffix-named object must not be inferred as a reference.
+ assertThat(
+ java.nio.file.Files.exists(
+ tempDir.resolve("manifest")
+ .resolve(ManifestSidecar.fileName(written))))
+ .isTrue();
+ String explicitName = "custom-index-name" + ManifestSidecar.SUFFIX;
+ java.nio.file.Files.move(
+ tempDir.resolve("manifest").resolve(ManifestSidecar.fileName(written)),
+ tempDir.resolve("manifest").resolve(explicitName));
+ String otherName = "other-partition-index";
+ java.nio.file.Path otherPath = tempDir.resolve("manifest").resolve(otherName);
+ java.nio.file.Files.write(otherPath, new byte[] {1, 2, 3});
+ ManifestFileMeta indexed = withExtraFiles(written, Arrays.asList(otherName, explicitName));
+ assertThat(manifests.selectBlocks(indexed, query).blocks()).isEmpty();
+ assertThat(io.opened)
+ .containsExactly(new Path(tempDir.toString(), "manifest/" + explicitName));
+ manifests.delete(indexed);
+ assertThat(java.nio.file.Files.exists(otherPath)).isFalse();
+ assertThat(java.nio.file.Files.exists(tempDir.resolve("manifest").resolve(explicitName)))
+ .isFalse();
+ assertThat(
+ java.nio.file.Files.exists(
+ tempDir.resolve("manifest").resolve(written.fileName())))
+ .isFalse();
+ }
+
+ @Test
+ void testDisabledOrUnfilteredReadsSkipSidecarMetadata() {
+ for (boolean enabled : new boolean[] {false, true}) {
+ Options options = new Options();
+ options.set(CoreOptions.MANIFEST_SORT_ENABLED, true);
+ options.set(CoreOptions.MANIFEST_SIDECAR_ENABLED, enabled);
+ RecordingFileIO io = new RecordingFileIO();
+ ManifestFile manifests =
+ createManifestFileFactory(tempDir.toString(), Long.MAX_VALUE, options, io)
+ .create();
+ ManifestFileMeta meta = mock(ManifestFileMeta.class);
+ RowRangeIndex rows =
+ enabled
+ ? null
+ : RowRangeIndex.create(Collections.singletonList(new Range(1, 1)));
+ assertThat(manifests.selectBlocks(meta, rows)).isNull();
+ verifyNoInteractions(meta);
+ assertThat(io.opened).isEmpty();
+ }
+ }
+
+ private List readSelectedEntries(
+ ManifestFile manifests, ManifestFileMeta meta, ManifestSidecar.Selection selected) {
+ return manifests.read(
+ meta.fileName(),
+ meta.fileSize(),
+ null,
+ null,
+ row -> true,
+ entry -> true,
+ java.util.function.Function.identity(),
+ selected);
+ }
+
+ /** Observes actual file access without adding counters to production readers. */
+ private static final class RecordingFileIO extends LocalFileIO {
+
+ private final List opened = Collections.synchronizedList(new ArrayList<>());
+ private final List seeks = Collections.synchronizedList(new ArrayList<>());
+ private final AtomicLong bytes = new AtomicLong();
+
+ private void reset() {
+ opened.clear();
+ seeks.clear();
+ bytes.set(0);
+ }
+
+ @Override
+ public SeekableInputStream newInputStream(Path path) throws IOException {
+ opened.add(path);
+ return new SeekableInputStreamWrapper(super.newInputStream(path)) {
+ @Override
+ public void seek(long desired) throws IOException {
+ seeks.add(desired);
+ super.seek(desired);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int value = super.read();
+ if (value >= 0) {
+ bytes.incrementAndGet();
+ }
+ return value;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ int n = super.read(buffer, offset, length);
+ if (n > 0) {
+ bytes.addAndGet(n);
+ }
+ return n;
+ }
+ };
+ }
+ }
+
private ManifestFile createManifestFile(String pathStr) {
return createManifestFile(pathStr, ThreadLocalRandom.current().nextInt(8192) + 1024);
}
private ManifestFile createManifestFile(String pathStr, long suggestedFileSize) {
- return createManifestFile(pathStr, suggestedFileSize, null);
+ return createManifestFile(pathStr, suggestedFileSize, new Options());
}
private ManifestFile createManifestFile(
String pathStr, long suggestedFileSize, @Nullable SegmentsCache cache) {
+ return createManifestFileFactory(
+ pathStr,
+ suggestedFileSize,
+ new Options(),
+ FileIOFinder.find(new Path(pathStr)),
+ cache)
+ .create();
+ }
+
+ private ManifestFile createManifestFile(
+ String pathStr, long suggestedFileSize, Options options) {
+ return createManifestFileFactory(
+ pathStr, suggestedFileSize, options, FileIOFinder.find(new Path(pathStr)))
+ .create();
+ }
+
+ private ManifestFile.Factory createManifestFileFactory(
+ String pathStr, long suggestedFileSize, Options options, FileIO fileIO) {
+ return createManifestFileFactory(pathStr, suggestedFileSize, options, fileIO, null);
+ }
+
+ private ManifestFile.Factory createManifestFileFactory(
+ String pathStr,
+ long suggestedFileSize,
+ Options options,
+ FileIO fileIO,
+ @Nullable SegmentsCache cache) {
+ return createManifestFileFactory(pathStr, suggestedFileSize, options, fileIO, cache, null);
+ }
+
+ private ManifestFile.Factory createManifestFileFactory(
+ String pathStr,
+ long suggestedFileSize,
+ Options options,
+ FileIO fileIO,
+ @Nullable SegmentsCache cache,
+ @Nullable SegmentsCache sidecarCache) {
Path path = new Path(pathStr);
FileStorePathFactory pathFactory =
new FileStorePathFactory(
@@ -1433,18 +2076,18 @@ private ManifestFile createManifestFile(
null,
false,
null);
- FileIO fileIO = FileIOFinder.find(path);
+ CoreOptions coreOptions = new CoreOptions(options);
return new ManifestFile.Factory(
- fileIO,
- new FileSystemSchemaManager(fileIO, path),
- DEFAULT_PART_TYPE,
- avro,
- "zstd",
- pathFactory,
- suggestedFileSize,
- cache,
- new CoreOptions(new Options()))
- .create();
+ fileIO,
+ new FileSystemSchemaManager(fileIO, path),
+ DEFAULT_PART_TYPE,
+ avro,
+ "zstd",
+ pathFactory,
+ suggestedFileSize,
+ cache,
+ sidecarCache,
+ coreOptions);
}
@Test
diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarTest.java
index 1110c7015861..1b3f9d258ee4 100644
--- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarTest.java
@@ -458,14 +458,14 @@ void cacheRespectsElementThreshold() throws Exception {
Files.write(temp.resolve(sidecar.getName()), data);
ManifestFileMeta meta = testMeta();
FileIO io = spy(LocalFileIO.create());
- SegmentsCache tooSmall =
+ SegmentsCache tooSmall =
new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), data.length - 1L, null, false);
assertThat(readCached(io, path, meta, tooSmall).blocks()).hasSize(2);
assertThat(readCached(io, path, meta, tooSmall).blocks()).hasSize(2);
assertThat(tooSmall.getIfPresents(sidecar)).isNull();
verify(io, times(2)).newInputStream(sidecar);
- SegmentsCache cache =
+ SegmentsCache cache =
new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), data.length, null, false);
assertThat(readCached(io, path, meta, cache).blocks()).hasSize(2);
assertThat(readCached(io, path, meta, cache).blocks()).hasSize(2);
@@ -478,7 +478,7 @@ void cachedSegmentsRequireSidecarType() throws Exception {
Path path = new Path(temp.toString(), "manifest-golden");
Path sidecar = ManifestSidecar.path(path);
Files.write(temp.resolve(sidecar.getName()), data);
- SegmentsCache cache =
+ SegmentsCache cache =
new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), Long.MAX_VALUE, null, false);
cache.put(sidecar, new SingleSegments(MemorySegment.wrap(data), data.length));
FileIO io = spy(LocalFileIO.create());
@@ -499,7 +499,7 @@ void missingAndInvalidSidecarsAreNotCached() throws Exception {
Path sidecar = ManifestSidecar.path(path);
ManifestFileMeta meta = testMeta();
FileIO io = spy(LocalFileIO.create());
- SegmentsCache cache =
+ SegmentsCache cache =
new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), Long.MAX_VALUE, null, false);
assertThat(readCached(io, path, meta, cache)).isNull();
assertThat(cache.getIfPresents(sidecar)).isNull();
@@ -521,7 +521,7 @@ void cachedBytesPreservePerQueryCancellation() throws Exception {
Files.write(temp.resolve(sidecar.getName()), testSidecar());
ManifestFileMeta meta = testMeta();
FileIO io = spy(LocalFileIO.create());
- SegmentsCache cache =
+ SegmentsCache cache =
new SegmentsCache<>(1024, MemorySize.ofMebiBytes(1), Long.MAX_VALUE, null, false);
RowRangeIndex cancelled = mock(RowRangeIndex.class);
when(cancelled.intersects(anyLong(), anyLong()))
@@ -544,7 +544,7 @@ void cachedBytesPreservePerQueryCancellation() throws Exception {
}
private ManifestSidecar.Selection readCached(
- FileIO io, Path path, ManifestFileMeta meta, SegmentsCache cache) {
+ FileIO io, Path path, ManifestFileMeta meta, SegmentsCache cache) {
return ManifestSidecar.read(
io,
path,
diff --git a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarWriteTest.java b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarWriteTest.java
index cea952943f59..3549bf6dcda1 100644
--- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarWriteTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestSidecarWriteTest.java
@@ -341,6 +341,7 @@ private ManifestFile manifests(
paths,
targetSize,
null,
+ null,
new CoreOptions(options))
.create();
}
diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java
index 20a1b26beece..dd1a1b176b96 100644
--- a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java
@@ -669,6 +669,7 @@ private ManifestFile createManifestFile(long suggestedFileSize) {
null),
suggestedFileSize,
null,
+ null,
new CoreOptions(new Options()))
.create();
}