feat: support zstd metadata compression - #2894
wirybeaver wants to merge 3 commits into
Conversation
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.
|
Small heads up just to make sure there aren’t any semantic merge conflicts here, it might be worth rebasing on main since #2887 Adds some codecs. |
a2b816e to
c8cb597
Compare
| pub(crate) const TABLE_METADATA_SUPPORTED_COMPRESSION: &[CompressionCodec] = &[ | ||
| CompressionCodec::None, | ||
| CompressionCodec::Gzip(GZIP_DEFAULT_LEVEL), | ||
| CompressionCodec::Zstd(ZSTD_DEFAULT_LEVEL), | ||
| ]; | ||
|
|
||
| pub(crate) const TABLE_METADATA_SUFFIX_TO_COMPRESSION: &[(&str, CompressionCodec)] = &[ | ||
| (ZSTD_SUFFIX, CompressionCodec::Zstd(ZSTD_DEFAULT_LEVEL)), | ||
| (GZIP_SUFFIX, CompressionCodec::Gzip(GZIP_DEFAULT_LEVEL)), | ||
| ]; | ||
|
|
||
| pub(crate) const TABLE_METADATA_MAGIC_TO_COMPRESSION: &[(&[u8], CompressionCodec)] = &[ | ||
| (GZIP_MAGIC, CompressionCodec::Gzip(GZIP_DEFAULT_LEVEL)), | ||
| (ZSTD_MAGIC, CompressionCodec::Zstd(ZSTD_DEFAULT_LEVEL)), | ||
| ]; |
There was a problem hiding this comment.
I think they should be const fn functions of CompressionCodec?
There was a problem hiding this comment.
Replaced the static metadata codec maps with associated const helpers on CompressionCodec in 89da77d.
The codec list, suffix mapping, and magic mapping now live behind CompressionCodec methods and are reused by property validation, location parsing, and content-based reads.
| serde_json::from_value(serde_json::Value::String(lowercase_value)) | ||
| .map_err(|_| invalid_metadata_compression_codec(value))?; | ||
|
|
||
| if TABLE_METADATA_SUPPORTED_COMPRESSION.contains(&codec) { |
There was a problem hiding this comment.
This is obviously incorrect, you missed compression level, see https://github.com/apache/iceberg-rust/pull/2894/changes#r3948429401
There was a problem hiding this comment.
Made metadata compression support validation level-aware in 89da77d.
The check now matches Gzip() and Zstd() instead of comparing against default-level enum values. Tests cover non-default Gzip and Zstd levels to prevent this regression.
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.
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.
c8cb597 to
89da77d
Compare
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