This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new edbe5061 feat(core): support structured-data local cache selection
(#972)
edbe5061 is described below
commit edbe50610b7c31b170f7d846a38fcc589a86b468
Author: XiaoHongbo <[email protected]>
AuthorDate: Tue Oct 20 17:40:54 2026 +0800
feat(core): support structured-data local cache selection (#972)
---
crates/paimon/src/io/cache/file_type.rs | 84 ++++++++++++++++++++++++++-----
crates/paimon/src/io/cache/mod.rs | 29 ++++++++++-
crates/paimon/src/io/file_io.rs | 88 ++++++++++++++++++++++++++++++++-
docs/src/getting-started.md | 6 ++-
4 files changed, 192 insertions(+), 15 deletions(-)
diff --git a/crates/paimon/src/io/cache/file_type.rs
b/crates/paimon/src/io/cache/file_type.rs
index e01a6e15..2e8b66e5 100644
--- a/crates/paimon/src/io/cache/file_type.rs
+++ b/crates/paimon/src/io/cache/file_type.rs
@@ -24,22 +24,24 @@ const TEMP_FILE_SUFFIX_LEN: usize = 41;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(super) enum FileType {
Meta,
- Data,
+ DedicatedData,
+ StructuredData,
BucketIndex,
GlobalIndex,
FileIndex,
}
impl FileType {
- const ALL: [Self; 5] = [
+ const ALL: [Self; 6] = [
Self::Meta,
- Self::Data,
+ Self::DedicatedData,
+ Self::StructuredData,
Self::BucketIndex,
Self::GlobalIndex,
Self::FileIndex,
];
- /// Mirrors Java `org.apache.paimon.utils.FileType#classify`, including
its check order.
+ /// Keeps metadata/index classification ahead of dedicated data detection.
pub(super) fn classify(path: &str) -> Self {
let mut segments = path.rsplit('/');
let name = unwrap_temp_file_name(segments.next().unwrap_or(path));
@@ -78,7 +80,17 @@ impl FileType {
return Self::Meta;
}
- Self::Data
+ if Self::is_dedicated_file(name) {
+ Self::DedicatedData
+ } else {
+ Self::StructuredData
+ }
+ }
+
+ /// Matches Java's BLOB/video and vector-store file naming rules.
+ fn is_dedicated_file(name: &str) -> bool {
+ let name = name.to_ascii_lowercase();
+ name.ends_with(".blob") || name.ends_with(".video") ||
name.contains(".vector.")
}
pub(super) fn is_mutable(path: &str) -> bool {
@@ -107,13 +119,15 @@ impl FileType {
"meta" => &[Self::Meta],
"global-index" => &[Self::GlobalIndex],
"bucket-index" => &[Self::BucketIndex],
- "data" => &[Self::Data],
+ // Preserve the existing broad data selector.
+ "data" => &[Self::DedicatedData, Self::StructuredData],
+ "structured-data" => &[Self::StructuredData],
"file-index" => &[Self::FileIndex],
"" => &[],
unknown => {
log::warn!(
"Unknown local-cache.whitelist value '{}';
supported values are \
- meta, global-index, bucket-index, data,
file-index, \
+ meta, global-index, bucket-index, data,
structured-data, file-index, \
or * for all of them",
unknown
);
@@ -168,7 +182,7 @@ mod tests {
),
(
"s3://bucket/table/bucket-0/data-abc.parquet",
- FileType::Data,
+ FileType::StructuredData,
),
(
"s3://bucket/table/manifest/manifest-123e4567-e89b-12d3-a456-426614174000-0.avro.sidecar",
@@ -182,7 +196,7 @@ mod tests {
("s3://bucket/table/changelog/changelog-5", FileType::Meta),
(
"s3://bucket/table/bucket-0/changelog-123e4567-e89b-12d3-a456-426614174000-0.parquet",
- FileType::Data,
+ FileType::StructuredData,
),
(
"s3://bucket/table/snapshot/.snapshot-13.123e4567-e89b-12d3-a456-426614174000.tmp",
@@ -194,7 +208,7 @@ mod tests {
),
(
"s3://bucket/table/bucket-0/.data-abc.parquet.123e4567-e89b-12d3-a456-426614174000.tmp",
- FileType::Data,
+ FileType::StructuredData,
),
];
@@ -248,13 +262,61 @@ mod tests {
}
}
+ #[test]
+ fn
test_dedicated_file_classification_keeps_metadata_and_indexes_separate() {
+ for name in [
+ "data.parquet",
+ "data.ORC",
+ "data.avro",
+ "data.row",
+ "data.mosaic",
+ "data.vortex",
+ "data.csv",
+ "data.json",
+ "data.text",
+ "data.csv.gz",
+ "data.JSON.ZST",
+ "data.text.snappy",
+ "data.unknown",
+ "dir.parquet/data",
+ ] {
+ assert_eq!(FileType::classify(name), FileType::StructuredData);
+ }
+ for name in [
+ "data.blob",
+ "data.BLOB",
+ "data.parquet.blob",
+ "data.video",
+ "data.VIDEO",
+ "data.vector.vortex",
+ "data.VECTOR.index-data",
+ ] {
+ assert_eq!(FileType::classify(name), FileType::DedicatedData);
+ }
+ assert_eq!(FileType::classify("manifest-1.avro"), FileType::Meta);
+ assert_eq!(FileType::classify("snapshot-1.parquet"), FileType::Meta);
+ assert_eq!(
+ FileType::classify("data.parquet.index"),
+ FileType::FileIndex
+ );
+ assert_eq!(
+ FileType::parse_whitelist("data"),
+ HashSet::from([FileType::DedicatedData, FileType::StructuredData])
+ );
+ assert_eq!(
+ FileType::parse_whitelist("structured-data"),
+ HashSet::from([FileType::StructuredData])
+ );
+ }
+
#[test]
fn test_file_type_parses_whitelist() {
let all = HashSet::from([
FileType::Meta,
FileType::GlobalIndex,
FileType::BucketIndex,
- FileType::Data,
+ FileType::DedicatedData,
+ FileType::StructuredData,
FileType::FileIndex,
]);
diff --git a/crates/paimon/src/io/cache/mod.rs
b/crates/paimon/src/io/cache/mod.rs
index 061cec19..74c636d0 100644
--- a/crates/paimon/src/io/cache/mod.rs
+++ b/crates/paimon/src/io/cache/mod.rs
@@ -306,7 +306,6 @@ impl LocalCacheConfig {
.get(CatalogOptions::LOCAL_CACHE_WHITELIST)
.map(String::as_str)
.unwrap_or("meta,global-index");
-
Ok(Some(Self {
dir,
namespace: catalog_namespace(namespace_options),
@@ -409,7 +408,11 @@ mod tests {
assert_eq!(config.block_size, 64 * 1024);
assert_eq!(
config.whitelist,
- std::collections::HashSet::from([FileType::Meta, FileType::Data])
+ std::collections::HashSet::from([
+ FileType::Meta,
+ FileType::DedicatedData,
+ FileType::StructuredData
+ ])
);
}
@@ -485,6 +488,28 @@ mod tests {
assert_eq!(cache.file_size(path, &token).await, None);
}
+ #[test]
+ fn test_structured_data_excludes_dedicated_and_mutable_files() {
+ let mut options = Options::new();
+ options.set(CatalogOptions::LOCAL_CACHE_ENABLED, "true");
+ options.set(CatalogOptions::LOCAL_CACHE_WHITELIST, "structured-data");
+ let cache = create_local_cache(&options).unwrap().unwrap();
+ assert!(cache.is_cacheable("s3://bucket/data.parquet"));
+ assert!(cache.is_cacheable("s3://bucket/data.unknown"));
+ for path in [
+ "s3://bucket/data.parquet.tmp",
+
"s3://bucket/.data.parquet.123e4567-e89b-12d3-a456-426614174000.tmp",
+ "s3://bucket/data.tmp.parquet",
+ "s3://bucket/manifest-1.avro",
+ "s3://bucket/data.parquet.index",
+ "s3://bucket/data.blob",
+ "s3://bucket/data.video",
+ "s3://bucket/data.vector.vortex",
+ ] {
+ assert!(!cache.is_cacheable(path), "{path}");
+ }
+ }
+
#[tokio::test]
async fn
test_local_cache_file_size_is_invalidated_across_shared_instances() {
let directory = tempfile::tempdir().unwrap();
diff --git a/crates/paimon/src/io/file_io.rs b/crates/paimon/src/io/file_io.rs
index e65ffadd..41cc2adb 100644
--- a/crates/paimon/src/io/file_io.rs
+++ b/crates/paimon/src/io/file_io.rs
@@ -268,7 +268,7 @@ impl FileIO {
/// `block_size` controls the aligned ranges presented to the cache.
/// `whitelist` uses the same comma-separated values as
/// `local-cache.whitelist`: `meta`, `global-index`, `bucket-index`,
`data`,
- /// and `file-index`, or `*` for all of them.
+ /// `structured-data`, and `file-index`, or `*` for all of them.
pub fn with_file_block_cache(
mut self,
cache: Arc<dyn FileBlockCache>,
@@ -2426,6 +2426,92 @@ mod input_output_test {
}
}
+ #[tokio::test]
+ async fn test_structured_data_cache_preserves_existing_selectors() {
+ for disk in [false, true] {
+ for whitelist in [
+ "meta,global-index",
+ "data",
+ "structured-data",
+ "meta,global-index,structured-data",
+ "*",
+ ] {
+ let directory = tempfile::tempdir().unwrap();
+ let mut options = Options::new();
+ options.set(CatalogOptions::LOCAL_CACHE_ENABLED, "true");
+ options.set(CatalogOptions::LOCAL_CACHE_WHITELIST, whitelist);
+ options.set(CatalogOptions::LOCAL_CACHE_BLOCK_SIZE, "4");
+ if disk {
+ options.set(
+ CatalogOptions::LOCAL_CACHE_DIR,
+ directory.path().to_string_lossy(),
+ );
+ }
+ let cache = Arc::new(
+
LocalCache::new(LocalCacheConfig::from_options(&options).unwrap().unwrap())
+ .unwrap(),
+ );
+ let file_io = FileIOBuilder::new("memory")
+ .with_local_cache(cache)
+ .build()
+ .unwrap();
+ for name in [
+ "data.parquet",
+ "data.blob",
+ "data.orc",
+ "data.avro",
+ "data.row",
+ "data.mosaic",
+ "data.vortex",
+ "data.csv",
+ "data.json.zst",
+ "data.text.snappy",
+ "data.video",
+ "data.vector.vortex",
+ "data.PARQUET",
+ "data.unknown",
+ "data.parquet.index",
+ "snapshot-1",
+ "snapshot-1.blob",
+ ] {
+ let path = format!("memory:/{name}");
+ file_io
+ .new_output(&path)
+ .unwrap()
+ .write(Bytes::from_static(b"abcdefgh"))
+ .await
+ .unwrap();
+ let input = file_io.new_input(&path).unwrap();
+ let reader = input.reader().await.unwrap();
+ assert_eq!(reader.read(1..7).await.unwrap(),
b"bcdefg"[..]);
+ drop(reader);
+ let (op, relative_path, _) =
input.source.resolve(&path).await.unwrap();
+ op.delete(&relative_path).await.unwrap();
+ let data_file = name.starts_with("data.") &&
!name.ends_with(".index");
+ let dedicated =
+ matches!(name, "data.blob" | "data.video" |
"data.vector.vortex");
+ let cached = match whitelist {
+ "*" => true,
+ "data" => data_file,
+ "structured-data" => data_file && !dedicated,
+ "meta,global-index,structured-data" => {
+ name.starts_with("snapshot-") || (data_file &&
!dedicated)
+ }
+ _ => name.starts_with("snapshot-"),
+ };
+ let result = input.read().await;
+ if cached {
+ assert_eq!(result.unwrap(), b"abcdefgh"[..],
"{whitelist}: {name}");
+ let reader = input.reader().await.unwrap();
+ assert_eq!(reader.read(1..7).await.unwrap(),
b"bcdefg"[..]);
+ } else {
+ assert!(result.is_err(), "{whitelist}: {name}");
+ }
+ }
+ }
+ }
+ }
+
async fn common_test_output_file_write_and_read(file_io: &FileIO, path:
&str) {
let output = file_io.new_output(path).unwrap();
let mut writer = output.writer().await.unwrap();
diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md
index 40216050..49fca6a1 100644
--- a/docs/src/getting-started.md
+++ b/docs/src/getting-started.md
@@ -403,7 +403,11 @@ let catalog = CatalogFactory::create(options).await?;
| `local-cache.dir` | none | Optional base directory. When set, Paimon uses a
persistent disk cache in a private versioned child directory; otherwise it uses
memory. |
| `local-cache.max-size` | unlimited | Maximum cache size. Memory caches count
payload bytes; disk caches count encoded bytes. Values accept byte units such
as `512 MiB` or `20 GiB`. |
| `local-cache.block-size` | `1 MiB` | Block size used for cached range reads.
|
-| `local-cache.whitelist` | `meta,global-index` | Comma-separated eligible
types: `meta`, `global-index`, `bucket-index`, `data`, and `file-index`, or `*`
for all of them. |
+| `local-cache.whitelist` | `meta,global-index` | Comma-separated eligible
types: `meta`, `global-index`, `bucket-index`, `data`, `structured-data`, and
`file-index`, or `*` for all of them. |
+
+Use `local-cache.whitelist=meta,global-index,structured-data` to cache data
+blocks without dedicated BLOB, video, or vector-store files. `data` and `*`
+retain their broader behavior.
Each catalog owns its in-memory cache for the catalog's lifetime. Disk caches
are reused after process restarts. Cache keys include a catalog-configuration