feat: support zstd metadata compression - #2894
Conversation
Add zstd metadata codec validation, canonical filename generation, alternate suffix parsing, and content-based frame detection for reads. Cover interoperability, codec transitions, truncated frames, and semantic round trips. Part of apache#2411.
xanderbailey
left a comment
There was a problem hiding this comment.
Nice PR, thanks for working on this! Left a comment on style, let me know what you think
| let zstd_suffix = CompressionCodec::zstd_default().suffix()?; | ||
| let metadata_suffix = ".metadata.json"; | ||
|
|
||
| let (stripped, compression_codec) = if let Some(stripped) = | ||
| file_name.strip_suffix(&format!("{metadata_suffix}{zstd_suffix}")) | ||
| { | ||
| (stripped, CompressionCodec::zstd_default()) | ||
| } else if let Some(stripped) = | ||
| file_name.strip_suffix(&format!("{metadata_suffix}{gzip_suffix}")) | ||
| { | ||
| (stripped, CompressionCodec::gzip_default()) | ||
| } else if let Some(stripped) = file_name.strip_suffix(metadata_suffix) { | ||
| if let Some(stripped) = stripped.strip_suffix(zstd_suffix) { | ||
| (stripped, CompressionCodec::zstd_default()) | ||
| } else if let Some(stripped) = stripped.strip_suffix(gzip_suffix) { | ||
| (stripped, CompressionCodec::gzip_default()) | ||
| } else { | ||
| (stripped, CompressionCodec::None) | ||
| } | ||
| } else { | ||
| (stripped, CompressionCodec::None) | ||
| return Err(Error::new( | ||
| ErrorKind::Unexpected, | ||
| format!("Invalid metadata file ending: {file_name}"), | ||
| )); |
There was a problem hiding this comment.
| let zstd_suffix = CompressionCodec::zstd_default().suffix()?; | |
| let metadata_suffix = ".metadata.json"; | |
| let (stripped, compression_codec) = if let Some(stripped) = | |
| file_name.strip_suffix(&format!("{metadata_suffix}{zstd_suffix}")) | |
| { | |
| (stripped, CompressionCodec::zstd_default()) | |
| } else if let Some(stripped) = | |
| file_name.strip_suffix(&format!("{metadata_suffix}{gzip_suffix}")) | |
| { | |
| (stripped, CompressionCodec::gzip_default()) | |
| } else if let Some(stripped) = file_name.strip_suffix(metadata_suffix) { | |
| if let Some(stripped) = stripped.strip_suffix(zstd_suffix) { | |
| (stripped, CompressionCodec::zstd_default()) | |
| } else if let Some(stripped) = stripped.strip_suffix(gzip_suffix) { | |
| (stripped, CompressionCodec::gzip_default()) | |
| } else { | |
| (stripped, CompressionCodec::None) | |
| } | |
| } else { | |
| (stripped, CompressionCodec::None) | |
| return Err(Error::new( | |
| ErrorKind::Unexpected, | |
| format!("Invalid metadata file ending: {file_name}"), | |
| )); | |
| let codecs = [ | |
| CompressionCodec::zstd_default(), | |
| CompressionCodec::gzip_default(), | |
| ]; | |
| let (stripped, compression_codec) = codecs | |
| .into_iter() | |
| .find_map(|codec| { | |
| strip_metadata_suffix(file_name, codec.suffix().ok()?).map(|s| (s, codec)) | |
| }) | |
| .or_else(|| { | |
| file_name | |
| .strip_suffix(METADATA_SUFFIX) | |
| .map(|s| (s, CompressionCodec::None)) | |
| }) | |
| .ok_or_else(|| { | |
| Error::new( | |
| ErrorKind::Unexpected, | |
| format!("Invalid metadata file ending: {file_name}"), | |
| ) | |
| })?; |
fn strip_metadata_suffix<'a>(file_name: &'a str, codec_suffix: &str) -> Option<&'a str> {
file_name
.strip_suffix(METADATA_SUFFIX)
.and_then(|s| s.strip_suffix(codec_suffix))
.or_else(|| {
file_name
.strip_suffix(codec_suffix)
.and_then(|s| s.strip_suffix(METADATA_SUFFIX))
})
}I wonder if this kind of thing reads a little better, this is very nested and hard to follow as it currently stands
There was a problem hiding this comment.
Done in fdc645c. I added the suggested strip_metadata_suffix helper and iterator-based parsing, using the shared suffix-to-codec mapping so the canonical and trailing forms stay easy to extend.
|
On closer look at the Java docs https://iceberg.apache.org/docs/latest/configuration/#write-properties, I don't actually see zstd here... am I missing something? |
|
Go has it... apache/iceberg-go#1020 They claim in that pr that py-iceberg and java have it but I can't find it... |
Yeah, I refer to the iceberg-go. And #2411 also mentioned this zstd support for metadata |
| let zstd_suffix = CompressionCodec::zstd_default().suffix()?; | ||
| let metadata_suffix = ".metadata.json"; | ||
|
|
||
| let (stripped, compression_codec) = if let Some(stripped) = | ||
| file_name.strip_suffix(&format!("{metadata_suffix}{zstd_suffix}")) | ||
| { | ||
| (stripped, CompressionCodec::zstd_default()) | ||
| } else if let Some(stripped) = | ||
| file_name.strip_suffix(&format!("{metadata_suffix}{gzip_suffix}")) | ||
| { | ||
| (stripped, CompressionCodec::gzip_default()) | ||
| } else if let Some(stripped) = file_name.strip_suffix(metadata_suffix) { | ||
| if let Some(stripped) = stripped.strip_suffix(zstd_suffix) { | ||
| (stripped, CompressionCodec::zstd_default()) | ||
| } else if let Some(stripped) = stripped.strip_suffix(gzip_suffix) { | ||
| (stripped, CompressionCodec::gzip_default()) | ||
| } else { | ||
| (stripped, CompressionCodec::None) | ||
| } | ||
| } else { | ||
| (stripped, CompressionCodec::None) | ||
| return Err(Error::new( | ||
| ErrorKind::Unexpected, | ||
| format!("Invalid metadata file ending: {file_name}"), | ||
| )); |
| CompressionCodec::None.name(), | ||
| CompressionCodec::gzip_default().name() | ||
| CompressionCodec::gzip_default().name(), | ||
| CompressionCodec::zstd_default().name() |
There was a problem hiding this comment.
I'm not a big fan of doing it this way, it's hard to extend and maitain. Ideally, we should have a static map in compression file, like TABLE_METADATA_SUFFIX_TO_COMPRESSION, TABLE_METADATA_MAGIC_TO_COMPRESSION, TABLE_METADATA_SUPPORTED_COMPreSSION, and reuse them in all places.
There was a problem hiding this comment.
Done in fdc645c. I centralized the supported metadata codecs, suffix mappings, and magic mappings in compression.rs, then reused them for property validation, location parsing, and content-based detection. The write path now relies on the validated codec directly as well.
Reuse shared supported-codec, suffix, and magic mappings across metadata property validation, location parsing, and I/O. Simplify alternate suffix parsing with a dedicated helper.
Which issue does this PR close?
Part of #2411. This implements the "Support zstd metadata compression codec" tracking item without closing the broader v3 epic.
What changes are included in this PR?
write.metadata.compression-codec=zstdcase-insensitively, using the existing default zstd level..zstd.metadata.jsonnames (.gz.metadata.jsonfor gzip). Trailing.metadata.json.zstdand.metadata.json.gzforms are accepted only when parsing existing locations for interoperability, and subsequent versions are canonicalized.No catalog-specific, dependency, lockfile, or format-version changes are included.
Are these changes tested?
Yes:
cargo test -p iceberg --lib— 1473 passedcargo fmt --all -- --checkcargo clippy -p iceberg --all-targets --all-features -- -D warnings