feat(session): configure DataFusion's built-in CacheManager from Java (#78)
diff --git a/core/src/main/java/org/apache/datafusion/CacheManagerOptions.java b/core/src/main/java/org/apache/datafusion/CacheManagerOptions.java
new file mode 100644
index 0000000..f217287
--- /dev/null
+++ b/core/src/main/java/org/apache/datafusion/CacheManagerOptions.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datafusion;
+
+import java.time.Duration;
+
+import org.apache.datafusion.protobuf.CacheManagerOptionsProto;
+import org.apache.datafusion.protobuf.ListFilesCacheOptionsProto;
+
+/**
+ * Configuration for DataFusion's built-in {@code CacheManager}. Pass an instance to {@link
+ * SessionContextBuilder#cacheManager(CacheManagerOptions)} to turn on any of the three
+ * upstream-provided caches at session construction time.
+ *
+ * <p>The three caches are independent; calling one setter does not affect the others. A setter that
+ * is never called leaves the upstream default in place — so existing callers that don't touch this
+ * builder see no behavioral change.
+ *
+ * <ul>
+ * <li>{@link Builder#fileMetadataCache(long)} — caches file-embedded metadata (e.g. parquet
+ * footers / page metadata). Backed by {@code DefaultFilesMetadataCache} with the supplied
+ * byte cap.
+ * <li>{@link Builder#listFilesCache(long, Duration)} — caches results of object-store {@code
+ * list} operations. Backed by {@code DefaultListFilesCache} with the supplied byte cap and
+ * optional TTL ({@code null} = infinite).
+ * <li>{@link Builder#fileStatisticsCache(boolean)} — caches per-file row counts and column
+ * statistics. Backed by {@code DefaultFileStatisticsCache}; no bytewise cap exists upstream.
+ * </ul>
+ *
+ * <p>This v1 surface configures the <em>built-in</em> cache implementations. Plugging custom Java
+ * cache implementations through a JNI upcall path is intentionally out of scope — every cache
+ * lookup is a hot path during scans, and routing them through Java would defeat the cache.
+ */
+public final class CacheManagerOptions {
+
+ private final Long fileMetadataCacheMaxBytes;
+ private final ListFilesCacheConfig listFilesCache;
+ private final Boolean fileStatisticsCacheEnabled;
+
+ private CacheManagerOptions(Builder b) {
+ this.fileMetadataCacheMaxBytes = b.fileMetadataCacheMaxBytes;
+ this.listFilesCache = b.listFilesCache;
+ this.fileStatisticsCacheEnabled = b.fileStatisticsCacheEnabled;
+ }
+
+ /** Begin building a {@link CacheManagerOptions} instance. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ CacheManagerOptionsProto toProto() {
+ CacheManagerOptionsProto.Builder b = CacheManagerOptionsProto.newBuilder();
+ if (fileMetadataCacheMaxBytes != null) {
+ b.setFileMetadataCacheMaxBytes(fileMetadataCacheMaxBytes);
+ }
+ if (listFilesCache != null) {
+ ListFilesCacheOptionsProto.Builder lb = ListFilesCacheOptionsProto.newBuilder();
+ if (listFilesCache.maxBytes != null) {
+ lb.setMaxBytes(listFilesCache.maxBytes);
+ }
+ if (listFilesCache.ttlMillis != null) {
+ lb.setTtlMillis(listFilesCache.ttlMillis);
+ }
+ b.setListFilesCache(lb.build());
+ }
+ if (fileStatisticsCacheEnabled != null) {
+ b.setFileStatisticsCacheEnabled(fileStatisticsCacheEnabled);
+ }
+ return b.build();
+ }
+
+ /** Internal carrier — `null`-vs-set is the source of truth for "user called this setter". */
+ private static final class ListFilesCacheConfig {
+ final Long maxBytes;
+ final Long ttlMillis;
+
+ ListFilesCacheConfig(Long maxBytes, Long ttlMillis) {
+ this.maxBytes = maxBytes;
+ this.ttlMillis = ttlMillis;
+ }
+ }
+
+ /** Builder for {@link CacheManagerOptions}. */
+ public static final class Builder {
+ private Long fileMetadataCacheMaxBytes;
+ private ListFilesCacheConfig listFilesCache;
+ private Boolean fileStatisticsCacheEnabled;
+
+ private Builder() {}
+
+ /**
+ * Enable the file-embedded metadata cache (parquet footers, page metadata) with the given byte
+ * cap. The cap is the budget the upstream {@code DefaultFilesMetadataCache} uses to evict
+ * entries; {@code 0} is legal and means "construct the cache but with a 0-byte budget"
+ * (effectively disabled but observable in stats).
+ *
+ * @throws IllegalArgumentException if {@code maxBytes} is negative.
+ */
+ public Builder fileMetadataCache(long maxBytes) {
+ if (maxBytes < 0) {
+ throw new IllegalArgumentException(
+ "fileMetadataCache maxBytes must be non-negative, got " + maxBytes);
+ }
+ this.fileMetadataCacheMaxBytes = maxBytes;
+ return this;
+ }
+
+ /**
+ * Enable the list-files cache with the given byte cap and TTL. Pass {@code null} for {@code
+ * ttl} to use upstream's "no expiration" semantics (entries are evicted only by capacity
+ * pressure).
+ *
+ * @throws IllegalArgumentException if {@code maxBytes} is negative or {@code ttl} is negative.
+ */
+ public Builder listFilesCache(long maxBytes, Duration ttl) {
+ if (maxBytes < 0) {
+ throw new IllegalArgumentException(
+ "listFilesCache maxBytes must be non-negative, got " + maxBytes);
+ }
+ Long ttlMillis = null;
+ if (ttl != null) {
+ if (ttl.isNegative()) {
+ throw new IllegalArgumentException("listFilesCache ttl must be non-negative, got " + ttl);
+ }
+ ttlMillis = ttl.toMillis();
+ }
+ this.listFilesCache = new ListFilesCacheConfig(maxBytes, ttlMillis);
+ return this;
+ }
+
+ /**
+ * Enable the file-statistics cache (per-file row counts / column statistics). When {@code
+ * enabled} is {@code true}, the upstream {@code DefaultFileStatisticsCache} is installed. When
+ * {@code false}, the slot is explicitly set to disabled in the wire format — the same end-state
+ * as never calling this setter, but distinguishable for testing.
+ */
+ public Builder fileStatisticsCache(boolean enabled) {
+ this.fileStatisticsCacheEnabled = enabled;
+ return this;
+ }
+
+ public CacheManagerOptions build() {
+ return new CacheManagerOptions(this);
+ }
+ }
+}
diff --git a/core/src/main/java/org/apache/datafusion/SessionContextBuilder.java b/core/src/main/java/org/apache/datafusion/SessionContextBuilder.java
index 10a5735..fb8ec3e 100644
--- a/core/src/main/java/org/apache/datafusion/SessionContextBuilder.java
+++ b/core/src/main/java/org/apache/datafusion/SessionContextBuilder.java
@@ -40,6 +40,7 @@
private Long memoryLimitBytes;
private Double memoryLimitFraction;
private String tempDirectory;
+ private CacheManagerOptions cacheManager;
private final LinkedHashMap<String, String> options = new LinkedHashMap<>();
private final List<ObjectStoreOptions> objectStores = new ArrayList<>();
@@ -172,6 +173,25 @@
}
/**
+ * Configure DataFusion's built-in {@code CacheManager} for the new context. Build the {@link
+ * CacheManagerOptions} via {@link CacheManagerOptions#builder()}; each cache slot is independent,
+ * so leaving a setter unset keeps the upstream default in place.
+ *
+ * <p>Calling this setter twice replaces the previous configuration — there is no incremental
+ * merge between calls. If you need a different cache configuration, build a new {@code
+ * CacheManagerOptions} from scratch.
+ *
+ * @throws IllegalArgumentException if {@code options} is {@code null}.
+ */
+ public SessionContextBuilder cacheManager(CacheManagerOptions options) {
+ if (options == null) {
+ throw new IllegalArgumentException("cacheManager options must be non-null");
+ }
+ this.cacheManager = options;
+ return this;
+ }
+
+ /**
* Register an {@code object_store::ObjectStore} backend on the new context's {@code RuntimeEnv}.
* Build {@link ObjectStoreOptions} via the per-backend factories ({@link ObjectStoreOptions#s3},
* {@link ObjectStoreOptions#gcs}, {@link ObjectStoreOptions#http(String)}). The store is
@@ -229,6 +249,9 @@
if (tempDirectory != null) {
b.setTempDirectory(tempDirectory);
}
+ if (cacheManager != null) {
+ b.setCacheManager(cacheManager.toProto());
+ }
for (Map.Entry<String, String> e : options.entrySet()) {
b.addOptions(ConfigOption.newBuilder().setKey(e.getKey()).setValue(e.getValue()).build());
}
diff --git a/core/src/test/java/org/apache/datafusion/CacheManagerOptionsTest.java b/core/src/test/java/org/apache/datafusion/CacheManagerOptionsTest.java
new file mode 100644
index 0000000..66417bc
--- /dev/null
+++ b/core/src/test/java/org/apache/datafusion/CacheManagerOptionsTest.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datafusion;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+
+import org.apache.datafusion.protobuf.CacheManagerOptionsProto;
+import org.apache.datafusion.protobuf.SessionOptions;
+import org.junit.jupiter.api.Test;
+
+class CacheManagerOptionsTest {
+
+ @Test
+ void allThreeCachesRoundTripThroughProto() throws Exception {
+ byte[] bytes =
+ SessionContext.builder()
+ .cacheManager(
+ CacheManagerOptions.builder()
+ .fileMetadataCache(64L << 20)
+ .listFilesCache(8L << 20, Duration.ofMinutes(5))
+ .fileStatisticsCache(true)
+ .build())
+ .toBytes();
+
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ assertTrue(parsed.hasCacheManager());
+ CacheManagerOptionsProto cm = parsed.getCacheManager();
+
+ assertTrue(cm.hasFileMetadataCacheMaxBytes());
+ assertEquals(64L << 20, cm.getFileMetadataCacheMaxBytes());
+
+ assertTrue(cm.hasListFilesCache());
+ assertTrue(cm.getListFilesCache().hasMaxBytes());
+ assertEquals(8L << 20, cm.getListFilesCache().getMaxBytes());
+ assertTrue(cm.getListFilesCache().hasTtlMillis());
+ assertEquals(Duration.ofMinutes(5).toMillis(), cm.getListFilesCache().getTtlMillis());
+
+ assertTrue(cm.hasFileStatisticsCacheEnabled());
+ assertTrue(cm.getFileStatisticsCacheEnabled());
+ }
+
+ @Test
+ void unsetSettersAreAbsentInProto() throws Exception {
+ byte[] bytes =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileMetadataCache(64L << 20).build())
+ .toBytes();
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ CacheManagerOptionsProto cm = parsed.getCacheManager();
+
+ assertTrue(cm.hasFileMetadataCacheMaxBytes());
+ // The other two travel as unset, not as zero/empty -- the Rust side
+ // distinguishes "leave upstream default in place" from "explicitly off".
+ assertFalse(cm.hasListFilesCache());
+ assertFalse(cm.hasFileStatisticsCacheEnabled());
+ }
+
+ @Test
+ void listFilesCacheNullTtlIsUnsetOnTheWire() throws Exception {
+ byte[] bytes =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().listFilesCache(8L << 20, null).build())
+ .toBytes();
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ CacheManagerOptionsProto cm = parsed.getCacheManager();
+ assertTrue(cm.hasListFilesCache());
+ assertTrue(cm.getListFilesCache().hasMaxBytes());
+ assertEquals(8L << 20, cm.getListFilesCache().getMaxBytes());
+ // ttl_millis is the channel for "infinite" (None on the Rust side) --
+ // it has to travel as unset, not as 0.
+ assertFalse(cm.getListFilesCache().hasTtlMillis());
+ }
+
+ @Test
+ void fileMetadataCacheZeroIsAccepted() throws Exception {
+ // Upstream allows 0 (the cache is constructed with a 0-byte budget).
+ // Java mirrors that contract.
+ byte[] bytes =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileMetadataCache(0).build())
+ .toBytes();
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ CacheManagerOptionsProto cm = parsed.getCacheManager();
+ assertTrue(cm.hasFileMetadataCacheMaxBytes());
+ assertEquals(0L, cm.getFileMetadataCacheMaxBytes());
+ }
+
+ @Test
+ void fileStatisticsCacheFalseTravelsExplicitlyOnTheWire() throws Exception {
+ // `fileStatisticsCache(false)` is a different end-state from "never
+ // called this setter" only on the wire -- the Rust side treats both as
+ // "leave the slot None" today, but distinguishing the two on the wire
+ // lets us assert the bool actually round-tripped.
+ byte[] bytes =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileStatisticsCache(false).build())
+ .toBytes();
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ CacheManagerOptionsProto cm = parsed.getCacheManager();
+ assertTrue(cm.hasFileStatisticsCacheEnabled());
+ assertFalse(cm.getFileStatisticsCacheEnabled());
+ }
+
+ @Test
+ void unsetCacheManagerIsAbsentInProto() throws Exception {
+ // Sanity check that builders that never call .cacheManager(...) emit no
+ // cache_manager field at all -- the existing-callers-see-no-change
+ // contract relies on this.
+ byte[] bytes = SessionContext.builder().batchSize(8192).toBytes();
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ assertFalse(parsed.hasCacheManager());
+ }
+
+ @Test
+ void rejectsNegativeFileMetadataCacheMaxBytes() {
+ CacheManagerOptions.Builder b = CacheManagerOptions.builder();
+ assertThrows(IllegalArgumentException.class, () -> b.fileMetadataCache(-1));
+ }
+
+ @Test
+ void rejectsNegativeListFilesCacheMaxBytes() {
+ CacheManagerOptions.Builder b = CacheManagerOptions.builder();
+ assertThrows(IllegalArgumentException.class, () -> b.listFilesCache(-1, null));
+ }
+
+ @Test
+ void rejectsNegativeListFilesCacheTtl() {
+ CacheManagerOptions.Builder b = CacheManagerOptions.builder();
+ assertThrows(IllegalArgumentException.class, () -> b.listFilesCache(0, Duration.ofMillis(-1)));
+ }
+
+ @Test
+ void cacheManagerRejectsNull() {
+ SessionContextBuilder b = SessionContext.builder();
+ assertThrows(IllegalArgumentException.class, () -> b.cacheManager(null));
+ }
+
+ @Test
+ void cacheManagerSetterReplacesPreviousValue() throws Exception {
+ // Setter replaces; doesn't merge. Calling fileMetadataCache(64) then
+ // listFilesCache(8) on a fresh builder must NOT carry the file-metadata
+ // setting forward.
+ byte[] bytes =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileMetadataCache(64L << 20).build())
+ .cacheManager(CacheManagerOptions.builder().listFilesCache(8L << 20, null).build())
+ .toBytes();
+ SessionOptions parsed = SessionOptions.parseFrom(bytes);
+ CacheManagerOptionsProto cm = parsed.getCacheManager();
+ // Only list-files survives -- the second .cacheManager(...) call replaced
+ // the first.
+ assertFalse(cm.hasFileMetadataCacheMaxBytes());
+ assertTrue(cm.hasListFilesCache());
+ }
+}
diff --git a/core/src/test/java/org/apache/datafusion/SessionContextCacheManagerTest.java b/core/src/test/java/org/apache/datafusion/SessionContextCacheManagerTest.java
new file mode 100644
index 0000000..619e469
--- /dev/null
+++ b/core/src/test/java/org/apache/datafusion/SessionContextCacheManagerTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datafusion;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Duration;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.junit.jupiter.api.Test;
+
+class SessionContextCacheManagerTest {
+
+ @Test
+ void fileMetadataCacheBuildsAndContextIsUsable() throws Exception {
+ try (BufferAllocator allocator = new RootAllocator();
+ SessionContext ctx =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileMetadataCache(64L << 20).build())
+ .build();
+ DataFrame df = ctx.sql("SELECT 1");
+ ArrowReader reader = df.collect(allocator)) {
+ assertTrue(reader.loadNextBatch());
+ assertEquals(1, reader.getVectorSchemaRoot().getRowCount());
+ }
+ }
+
+ @Test
+ void listFilesCacheWithFiniteTtlBuilds() {
+ try (SessionContext ctx =
+ SessionContext.builder()
+ .cacheManager(
+ CacheManagerOptions.builder()
+ .listFilesCache(8L << 20, Duration.ofMinutes(5))
+ .build())
+ .build()) {
+ assertNotNull(ctx);
+ }
+ }
+
+ @Test
+ void listFilesCacheWithInfiniteTtlBuilds() {
+ // null TTL must reach the Rust side as None (infinite). Construction
+ // succeeds; the only failure mode would be the JNI layer mistranslating
+ // null as "TTL=0" and the upstream constructor rejecting it.
+ try (SessionContext ctx =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().listFilesCache(8L << 20, null).build())
+ .build()) {
+ assertNotNull(ctx);
+ }
+ }
+
+ @Test
+ void fileStatisticsCacheBuilds() {
+ try (SessionContext ctx =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileStatisticsCache(true).build())
+ .build()) {
+ assertNotNull(ctx);
+ }
+ }
+
+ @Test
+ void allThreeCachesTogetherBuildAndContextIsUsable() throws Exception {
+ try (BufferAllocator allocator = new RootAllocator();
+ SessionContext ctx =
+ SessionContext.builder()
+ .cacheManager(
+ CacheManagerOptions.builder()
+ .fileMetadataCache(64L << 20)
+ .listFilesCache(8L << 20, Duration.ofMinutes(5))
+ .fileStatisticsCache(true)
+ .build())
+ .build();
+ DataFrame df = ctx.sql("SELECT 1");
+ ArrowReader reader = df.collect(allocator)) {
+ assertTrue(reader.loadNextBatch());
+ assertEquals(1, reader.getVectorSchemaRoot().getRowCount());
+ }
+ }
+
+ @Test
+ void emptyCacheManagerOptionsIsHarmlessNoop() {
+ // builder().build() with nothing set is on the wire as "all three slots
+ // unset". The Rust side treats that as "skip with_cache_manager
+ // entirely" and the context construction succeeds.
+ try (SessionContext ctx =
+ SessionContext.builder().cacheManager(CacheManagerOptions.builder().build()).build()) {
+ assertNotNull(ctx);
+ }
+ }
+
+ @Test
+ void fileMetadataCacheZeroIsHarmless() {
+ // Upstream allows a 0-byte cap (cache constructed but evicts every
+ // insert). Java should not reject it.
+ try (SessionContext ctx =
+ SessionContext.builder()
+ .cacheManager(CacheManagerOptions.builder().fileMetadataCache(0).build())
+ .build()) {
+ assertNotNull(ctx);
+ }
+ }
+}
diff --git a/native/build.rs b/native/build.rs
index e19ce5b..d292514 100644
--- a/native/build.rs
+++ b/native/build.rs
@@ -18,6 +18,7 @@
fn main() {
const PROTOS: &[&str] = &[
"../proto/session_options.proto",
+ "../proto/cache_manager_options.proto",
"../proto/file_compression_type.proto",
"../proto/arrow_read_options.proto",
"../proto/avro_read_options.proto",
diff --git a/native/src/cache_manager.rs b/native/src/cache_manager.rs
new file mode 100644
index 0000000..3b9e286
--- /dev/null
+++ b/native/src/cache_manager.rs
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Translate the [`CacheManagerOptionsProto`] message into a
+//! [`CacheManagerConfig`] for [`RuntimeEnvBuilder::with_cache_manager`].
+//!
+//! Each of the three caches is independent; an unset proto field leaves the
+//! corresponding upstream default in place (no cache for list-files / stats,
+//! a `DefaultFilesMetadataCache` with the default limit for file metadata).
+//! When a setter *is* present, the JNI layer always installs a fresh
+//! `Default*Cache` impl -- the v1 contract is "configure the built-in
+//! caches", not "swap in a custom one".
+
+use std::sync::Arc;
+use std::time::Duration;
+
+use datafusion::execution::cache::cache_manager::CacheManagerConfig;
+use datafusion::execution::cache::cache_unit::{
+ DefaultFileStatisticsCache, DefaultFilesMetadataCache,
+};
+use datafusion::execution::cache::DefaultListFilesCache;
+
+use crate::errors::JniResult;
+use crate::proto_gen::CacheManagerOptionsProto;
+
+/// Build a [`CacheManagerConfig`] from the proto. Returns `Ok(None)` if the
+/// caller did not set any cache-manager field, so the JNI layer can skip the
+/// `with_cache_manager(...)` call entirely and let upstream's own
+/// `RuntimeEnvBuilder` defaults apply.
+pub(crate) fn build_config(
+ opts: &CacheManagerOptionsProto,
+) -> JniResult<Option<CacheManagerConfig>> {
+ // Treat "all three slots unset" as "caller didn't pass a cache_manager
+ // message at all" -- avoids overriding upstream's cache-manager-default
+ // with a freshly-constructed-but-empty config that happens to be
+ // equivalent today but might diverge on a future DataFusion release.
+ if opts.file_metadata_cache_max_bytes.is_none()
+ && opts.list_files_cache.is_none()
+ && opts.file_statistics_cache_enabled.is_none()
+ {
+ return Ok(None);
+ }
+
+ let mut config = CacheManagerConfig::default();
+
+ if let Some(max_bytes) = opts.file_metadata_cache_max_bytes {
+ let max = max_bytes as usize;
+ config.file_metadata_cache = Some(Arc::new(DefaultFilesMetadataCache::new(max)));
+ config.metadata_cache_limit = max;
+ }
+
+ if let Some(lfc) = &opts.list_files_cache {
+ // `max_bytes` unset → use upstream's documented default
+ // (currently 1 MiB; pulled from CacheManagerConfig::default()
+ // rather than re-declared here so we track upstream automatically).
+ let default_limit = CacheManagerConfig::default().list_files_cache_limit;
+ let max = lfc.max_bytes.map(|v| v as usize).unwrap_or(default_limit);
+ let ttl = lfc.ttl_millis.map(Duration::from_millis);
+
+ config.list_files_cache = Some(Arc::new(DefaultListFilesCache::new(max, ttl)));
+ config.list_files_cache_limit = max;
+ config.list_files_cache_ttl = ttl;
+ }
+
+ if opts.file_statistics_cache_enabled.unwrap_or(false) {
+ config.table_files_statistics_cache = Some(Arc::new(DefaultFileStatisticsCache::default()));
+ }
+
+ Ok(Some(config))
+}
diff --git a/native/src/lib.rs b/native/src/lib.rs
index b59d4fd..c4df7c0 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -17,6 +17,7 @@
mod arrow;
mod avro;
+mod cache_manager;
mod csv;
mod errors;
mod jni_util;
@@ -130,6 +131,12 @@
runtime_builder = runtime_builder.with_temp_file_path(PathBuf::from(dir));
}
+ if let Some(cm_opts) = opts.cache_manager.as_ref() {
+ if let Some(cm_config) = crate::cache_manager::build_config(cm_opts)? {
+ runtime_builder = runtime_builder.with_cache_manager(cm_config);
+ }
+ }
+
// datafusion.runtime.* keys live on RuntimeEnv (separate object from
// SessionConfig) and round-tripping them through getOption/setOption
// has subtle correctness pitfalls (lazy default-tempdir creation,
diff --git a/proto/cache_manager_options.proto b/proto/cache_manager_options.proto
new file mode 100644
index 0000000..208f0bd
--- /dev/null
+++ b/proto/cache_manager_options.proto
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+syntax = "proto3";
+
+package datafusion_java;
+
+option java_package = "org.apache.datafusion.protobuf";
+option java_multiple_files = true;
+
+// Configuration for DataFusion's built-in CacheManager (see
+// `datafusion::execution::cache::cache_manager::CacheManagerConfig`).
+// Each of the three caches is independent; unset fields leave the
+// upstream default in place. When a typed setter is used, the JNI
+// layer installs the corresponding `Default*Cache` implementation.
+//
+// The Java public class is `CacheManagerOptions`; the proto message
+// gets a `Proto` suffix to avoid a name clash on the Java side, the
+// same convention used for `ParquetReadOptionsProto` etc.
+message CacheManagerOptionsProto {
+ // file_metadata_cache: when set, install `DefaultFilesMetadataCache`
+ // configured with this byte cap and raise `metadata_cache_limit` to
+ // match. Unset = leave the upstream default in place. Zero is a
+ // legal value upstream and means "construct the cache but with a
+ // 0-byte budget" (effectively disabled but observable in stats).
+ optional uint64 file_metadata_cache_max_bytes = 1;
+
+ // list_files_cache: when set, install `DefaultListFilesCache`. The
+ // inner message's own fields preserve unset-ness so the caller can
+ // enable the cache with all-defaults by sending an empty
+ // ListFilesCacheOptionsProto.
+ optional ListFilesCacheOptionsProto list_files_cache = 2;
+
+ // file_statistics_cache: when true, install
+ // `DefaultFileStatisticsCache`. When false or unset, leave the slot
+ // `None` (disabled). No bytewise cap because upstream's
+ // `DefaultFileStatisticsCache` doesn't expose one.
+ optional bool file_statistics_cache_enabled = 3;
+}
+
+message ListFilesCacheOptionsProto {
+ // Memory cap. Unset = upstream default
+ // (`DEFAULT_LIST_FILES_CACHE_MEMORY_LIMIT`, currently 1 MiB).
+ optional uint64 max_bytes = 1;
+ // TTL in milliseconds. Unset = upstream's `None` (infinite TTL).
+ optional uint64 ttl_millis = 2;
+}
diff --git a/proto/session_options.proto b/proto/session_options.proto
index 330fe9b..c59b65a 100644
--- a/proto/session_options.proto
+++ b/proto/session_options.proto
@@ -19,6 +19,7 @@
package datafusion_java;
+import "cache_manager_options.proto";
import "object_store_options.proto";
option java_package = "org.apache.datafusion.protobuf";
@@ -57,6 +58,9 @@
// later one wins (matching upstream's `RuntimeEnv::register_object_store`
// semantics).
repeated ObjectStoreRegistration object_stores = 8;
+ // Configuration for DataFusion's built-in CacheManager. Unset = leave
+ // the upstream default in place (matches existing callers).
+ optional CacheManagerOptionsProto cache_manager = 9;
}
message ConfigOption {