This is an automated email from the ASF dual-hosted git repository.

mgrigorov pushed a commit to branch branch-1.12
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/branch-1.12 by this push:
     new 1d51c738e AVRO-4032: [Rust] Make it possible to set custom compression 
level for all codec that support it (#3095)
1d51c738e is described below

commit 1d51c738e6ada9d2d776ead8b078319ac12ef596
Author: MarcoLugo <[email protected]>
AuthorDate: Thu Aug 15 15:49:14 2024 -0400

    AVRO-4032: [Rust] Make it possible to set custom compression level for all 
codec that support it (#3095)
    
    * AVRO-4032: add zstd variant that allows for level config
    
    * add docs
    
    * manual impl for IntoStaticStr to handle 2 variants with same value
    
    * AVRO-4032: [Rust] Add codec settings for Bzip2
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-4032: [Rust] Add settings for Xz codec
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * add zstd settings
    
    * make our new settings structs visible
    
    * AVRO-4032: [Rust] Add support for Zstandard dictionary
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-4032: [Rust] Store the codec compression_level in the header metadata
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-4032: [Rust] Minor cleanups
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-4032: Fix the build
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * AVRO-4032: Add tests for Codec::Null and ::Deflate
    
    This way there is no need to enable other codecs when running unrelated 
tests.
    Otherwise the build fails with:
    ```
    error: function `avro_4032_codec_settings` is never used
      --> avro/tests/codecs.rs:50:4
       |
    50 | fn avro_4032_codec_settings(codec: Codec) -> TestResult {
       |    ^^^^^^^^^^^^^^^^^^^^^^^^
    ```
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    ---------
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    Co-authored-by: Martin Tzvetanov Grigorov <[email protected]>
    (cherry picked from commit 9379fdb9f5ca27887dfe67daac137176811f2b82)
---
 lang/rust/avro/src/codec.rs    | 157 +++++++++++++++++++++++++++++++----------
 lang/rust/avro/src/lib.rs      |   6 ++
 lang/rust/avro/src/reader.rs   |  50 ++++++++++---
 lang/rust/avro/src/writer.rs   |  24 +++++++
 lang/rust/avro/tests/codecs.rs |  89 +++++++++++++++++++++++
 5 files changed, 281 insertions(+), 45 deletions(-)

diff --git a/lang/rust/avro/src/codec.rs b/lang/rust/avro/src/codec.rs
index a394cad25..9ec9d88c6 100644
--- a/lang/rust/avro/src/codec.rs
+++ b/lang/rust/avro/src/codec.rs
@@ -21,18 +21,6 @@ use libflate::deflate::{Decoder, Encoder};
 use std::io::{Read, Write};
 use strum_macros::{EnumIter, EnumString, IntoStaticStr};
 
-#[cfg(feature = "bzip")]
-use bzip2::{
-    read::{BzDecoder, BzEncoder},
-    Compression,
-};
-#[cfg(feature = "snappy")]
-extern crate crc32fast;
-#[cfg(feature = "snappy")]
-use crc32fast::Hasher;
-#[cfg(feature = "xz")]
-use xz2::read::{XzDecoder, XzEncoder};
-
 /// The compression codec used to compress blocks.
 #[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, EnumString, 
IntoStaticStr)]
 #[strum(serialize_all = "kebab_case")]
@@ -49,15 +37,16 @@ pub enum Codec {
     /// CRC32 checksum of the uncompressed data in the block.
     Snappy,
     #[cfg(feature = "zstandard")]
-    Zstandard,
+    /// The `Zstandard` codec uses Facebook's 
[Zstandard](https://facebook.github.io/zstd/)
+    Zstandard(zstandard::ZstandardSettings),
     #[cfg(feature = "bzip")]
     /// The `BZip2` codec uses [BZip2](https://sourceware.org/bzip2/)
     /// compression library.
-    Bzip2,
+    Bzip2(bzip::Bzip2Settings),
     #[cfg(feature = "xz")]
     /// The `Xz` codec uses [Xz utils](https://tukaani.org/xz/)
     /// compression library.
-    Xz,
+    Xz(xz::XzSettings),
 }
 
 impl From<Codec> for Value {
@@ -87,7 +76,7 @@ impl Codec {
                     .compress(&stream[..], &mut encoded[..])
                     .map_err(Error::SnappyCompress)?;
 
-                let mut hasher = Hasher::new();
+                let mut hasher = crc32fast::Hasher::new();
                 hasher.update(&stream[..]);
                 let checksum = hasher.finalize();
                 let checksum_as_bytes = checksum.to_be_bytes();
@@ -98,22 +87,26 @@ impl Codec {
                 *stream = encoded;
             }
             #[cfg(feature = "zstandard")]
-            Codec::Zstandard => {
-                let mut encoder = zstd::Encoder::new(Vec::new(), 0).unwrap();
+            Codec::Zstandard(settings) => {
+                let mut encoder =
+                    zstd::Encoder::new(Vec::new(), settings.compression_level 
as i32).unwrap();
                 encoder.write_all(stream).map_err(Error::ZstdCompress)?;
                 *stream = encoder.finish().unwrap();
             }
             #[cfg(feature = "bzip")]
-            Codec::Bzip2 => {
-                let mut encoder = BzEncoder::new(&stream[..], 
Compression::best());
+            Codec::Bzip2(settings) => {
+                use bzip2::read::BzEncoder;
+
+                let mut encoder = BzEncoder::new(&stream[..], 
settings.compression());
                 let mut buffer = Vec::new();
                 encoder.read_to_end(&mut buffer).unwrap();
                 *stream = buffer;
             }
             #[cfg(feature = "xz")]
-            Codec::Xz => {
-                let compression_level = 9;
-                let mut encoder = XzEncoder::new(&stream[..], 
compression_level);
+            Codec::Xz(settings) => {
+                use xz2::read::XzEncoder;
+
+                let mut encoder = XzEncoder::new(&stream[..], 
settings.compression_level as u32);
                 let mut buffer = Vec::new();
                 encoder.read_to_end(&mut buffer).unwrap();
                 *stream = buffer;
@@ -148,7 +141,7 @@ impl Codec {
                 last_four.copy_from_slice(&stream[(stream.len() - 4)..]);
                 let expected: u32 = u32::from_be_bytes(last_four);
 
-                let mut hasher = Hasher::new();
+                let mut hasher = crc32fast::Hasher::new();
                 hasher.update(&decoded);
                 let actual = hasher.finalize();
 
@@ -158,21 +151,30 @@ impl Codec {
                 decoded
             }
             #[cfg(feature = "zstandard")]
-            Codec::Zstandard => {
+            Codec::Zstandard(_settings) => {
+                use std::io::BufReader;
+                use zstd::zstd_safe;
+
                 let mut decoded = Vec::new();
-                let mut decoder = zstd::Decoder::new(&stream[..]).unwrap();
+                let buffer_size = zstd_safe::DCtx::in_size();
+                let buffer = BufReader::with_capacity(buffer_size, 
&stream[..]);
+                let mut decoder = zstd::Decoder::new(buffer).unwrap();
                 std::io::copy(&mut decoder, &mut 
decoded).map_err(Error::ZstdDecompress)?;
                 decoded
             }
             #[cfg(feature = "bzip")]
-            Codec::Bzip2 => {
+            Codec::Bzip2(_) => {
+                use bzip2::read::BzDecoder;
+
                 let mut decoder = BzDecoder::new(&stream[..]);
                 let mut decoded = Vec::new();
                 decoder.read_to_end(&mut decoded).unwrap();
                 decoded
             }
             #[cfg(feature = "xz")]
-            Codec::Xz => {
+            Codec::Xz(_) => {
+                use xz2::read::XzDecoder;
+
                 let mut decoder = XzDecoder::new(&stream[..]);
                 let mut decoded: Vec<u8> = Vec::new();
                 decoder.read_to_end(&mut decoded).unwrap();
@@ -183,6 +185,72 @@ impl Codec {
     }
 }
 
+#[cfg(feature = "bzip")]
+pub mod bzip {
+    use bzip2::Compression;
+
+    #[derive(Clone, Copy, Eq, PartialEq, Debug)]
+    pub struct Bzip2Settings {
+        pub compression_level: u8,
+    }
+
+    impl Bzip2Settings {
+        pub fn new(compression_level: u8) -> Self {
+            Self { compression_level }
+        }
+
+        pub(crate) fn compression(&self) -> Compression {
+            Compression::new(self.compression_level as u32)
+        }
+    }
+
+    impl Default for Bzip2Settings {
+        fn default() -> Self {
+            Bzip2Settings::new(Compression::best().level() as u8)
+        }
+    }
+}
+
+#[cfg(feature = "zstandard")]
+pub mod zstandard {
+    #[derive(Clone, Copy, Eq, PartialEq, Debug)]
+    pub struct ZstandardSettings {
+        pub compression_level: u8,
+    }
+
+    impl ZstandardSettings {
+        pub fn new(compression_level: u8) -> Self {
+            Self { compression_level }
+        }
+    }
+
+    impl Default for ZstandardSettings {
+        fn default() -> Self {
+            Self::new(0)
+        }
+    }
+}
+
+#[cfg(feature = "xz")]
+pub mod xz {
+    #[derive(Clone, Copy, Eq, PartialEq, Debug)]
+    pub struct XzSettings {
+        pub compression_level: u8,
+    }
+
+    impl XzSettings {
+        pub fn new(compression_level: u8) -> Self {
+            Self { compression_level }
+        }
+    }
+
+    impl Default for XzSettings {
+        fn default() -> Self {
+            XzSettings::new(9)
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -216,19 +284,19 @@ mod tests {
     #[cfg(feature = "zstandard")]
     #[test]
     fn zstd_compress_and_decompress() -> TestResult {
-        compress_and_decompress(Codec::Zstandard)
+        
compress_and_decompress(Codec::Zstandard(zstandard::ZstandardSettings::default()))
     }
 
     #[cfg(feature = "bzip")]
     #[test]
     fn bzip_compress_and_decompress() -> TestResult {
-        compress_and_decompress(Codec::Bzip2)
+        compress_and_decompress(Codec::Bzip2(bzip::Bzip2Settings::default()))
     }
 
     #[cfg(feature = "xz")]
     #[test]
     fn xz_compress_and_decompress() -> TestResult {
-        compress_and_decompress(Codec::Xz)
+        compress_and_decompress(Codec::Xz(xz::XzSettings::default()))
     }
 
     fn compress_and_decompress(codec: Codec) -> TestResult {
@@ -250,13 +318,19 @@ mod tests {
         assert_eq!(<&str>::from(Codec::Snappy), "snappy");
 
         #[cfg(feature = "zstandard")]
-        assert_eq!(<&str>::from(Codec::Zstandard), "zstandard");
+        assert_eq!(
+            
<&str>::from(Codec::Zstandard(zstandard::ZstandardSettings::default())),
+            "zstandard"
+        );
 
         #[cfg(feature = "bzip")]
-        assert_eq!(<&str>::from(Codec::Bzip2), "bzip2");
+        assert_eq!(
+            <&str>::from(Codec::Bzip2(bzip::Bzip2Settings::default())),
+            "bzip2"
+        );
 
         #[cfg(feature = "xz")]
-        assert_eq!(<&str>::from(Codec::Xz), "xz");
+        assert_eq!(<&str>::from(Codec::Xz(xz::XzSettings::default())), "xz");
     }
 
     #[test]
@@ -270,13 +344,22 @@ mod tests {
         assert_eq!(Codec::from_str("snappy").unwrap(), Codec::Snappy);
 
         #[cfg(feature = "zstandard")]
-        assert_eq!(Codec::from_str("zstandard").unwrap(), Codec::Zstandard);
+        assert_eq!(
+            Codec::from_str("zstandard").unwrap(),
+            Codec::Zstandard(zstandard::ZstandardSettings::default())
+        );
 
         #[cfg(feature = "bzip")]
-        assert_eq!(Codec::from_str("bzip2").unwrap(), Codec::Bzip2);
+        assert_eq!(
+            Codec::from_str("bzip2").unwrap(),
+            Codec::Bzip2(bzip::Bzip2Settings::default())
+        );
 
         #[cfg(feature = "xz")]
-        assert_eq!(Codec::from_str("xz").unwrap(), Codec::Xz);
+        assert_eq!(
+            Codec::from_str("xz").unwrap(),
+            Codec::Xz(xz::XzSettings::default())
+        );
 
         assert!(Codec::from_str("not a codec").is_err());
     }
diff --git a/lang/rust/avro/src/lib.rs b/lang/rust/avro/src/lib.rs
index e00b9d830..7f7f57f3b 100644
--- a/lang/rust/avro/src/lib.rs
+++ b/lang/rust/avro/src/lib.rs
@@ -867,6 +867,12 @@ pub use crate::{
         serde_avro_slice, serde_avro_slice_opt,
     },
 };
+#[cfg(feature = "bzip")]
+pub use codec::bzip::Bzip2Settings;
+#[cfg(feature = "xz")]
+pub use codec::xz::XzSettings;
+#[cfg(feature = "zstandard")]
+pub use codec::zstandard::ZstandardSettings;
 pub use codec::Codec;
 pub use de::from_value;
 pub use decimal::Decimal;
diff --git a/lang/rust/avro/src/reader.rs b/lang/rust/avro/src/reader.rs
index 121f8e257..39c3473c8 100644
--- a/lang/rust/avro/src/reader.rs
+++ b/lang/rust/avro/src/reader.rs
@@ -75,8 +75,6 @@ impl<'r, R: Read> Block<'r, R> {
     /// Try to read the header and to set the writer `Schema`, the `Codec` and 
the marker based on
     /// its content.
     fn read_header(&mut self) -> AvroResult<()> {
-        let meta_schema = Schema::map(Schema::Bytes);
-
         let mut buf = [0u8; 4];
         self.reader
             .read_exact(&mut buf)
@@ -86,12 +84,16 @@ impl<'r, R: Read> Block<'r, R> {
             return Err(Error::HeaderMagic);
         }
 
+        let meta_schema = Schema::map(Schema::Bytes);
         if let Value::Map(metadata) = decode(&meta_schema, &mut self.reader)? {
             self.read_writer_schema(&metadata)?;
             self.codec = read_codec(&metadata)?;
 
             for (key, value) in metadata {
-                if key == "avro.schema" || key == "avro.codec" {
+                if key == "avro.schema"
+                    || key == "avro.codec"
+                    || key == "avro.codec.compression_level"
+                {
                     // already processed
                 } else if key.starts_with("avro.") {
                     warn!("Ignoring unknown metadata key: {}", key);
@@ -262,16 +264,48 @@ fn read_codec(metadata: &HashMap<String, Value>) -> 
AvroResult<Codec> {
         })
         .map(|codec_res| match codec_res {
             Ok(codec) => match Codec::from_str(codec) {
-                Ok(codec) => Ok(codec),
+                Ok(codec) => match codec {
+                    #[cfg(feature = "bzip")]
+                    Codec::Bzip2(_) => {
+                        use crate::Bzip2Settings;
+                        if let Some(Value::Bytes(bytes)) =
+                            metadata.get("avro.codec.compression_level")
+                        {
+                            Ok(Codec::Bzip2(Bzip2Settings::new(bytes[0])))
+                        } else {
+                            Ok(codec)
+                        }
+                    }
+                    #[cfg(feature = "xz")]
+                    Codec::Xz(_) => {
+                        use crate::XzSettings;
+                        if let Some(Value::Bytes(bytes)) =
+                            metadata.get("avro.codec.compression_level")
+                        {
+                            Ok(Codec::Xz(XzSettings::new(bytes[0])))
+                        } else {
+                            Ok(codec)
+                        }
+                    }
+                    #[cfg(feature = "zstandard")]
+                    Codec::Zstandard(_) => {
+                        use crate::ZstandardSettings;
+                        if let Some(Value::Bytes(bytes)) =
+                            metadata.get("avro.codec.compression_level")
+                        {
+                            
Ok(Codec::Zstandard(ZstandardSettings::new(bytes[0])))
+                        } else {
+                            Ok(codec)
+                        }
+                    }
+                    _ => Ok(codec),
+                },
                 Err(_) => Err(Error::CodecNotSupported(codec.to_owned())),
             },
             Err(err) => Err(err),
         });
 
-    match result {
-        Some(res) => res,
-        None => Ok(Codec::Null),
-    }
+    result.unwrap_or_else(|| Ok(Codec::Null))
 }
 
 /// Main interface for reading Avro formatted values.
diff --git a/lang/rust/avro/src/writer.rs b/lang/rust/avro/src/writer.rs
index 02938d694..976193a81 100644
--- a/lang/rust/avro/src/writer.rs
+++ b/lang/rust/avro/src/writer.rs
@@ -369,6 +369,30 @@ impl<'a, W: Write> Writer<'a, W> {
         let mut metadata = HashMap::with_capacity(2);
         metadata.insert("avro.schema", Value::Bytes(schema_bytes));
         metadata.insert("avro.codec", self.codec.into());
+        match self.codec {
+            #[cfg(feature = "bzip")]
+            Codec::Bzip2(settings) => {
+                metadata.insert(
+                    "avro.codec.compression_level",
+                    Value::Bytes(vec![settings.compression_level]),
+                );
+            }
+            #[cfg(feature = "xz")]
+            Codec::Xz(settings) => {
+                metadata.insert(
+                    "avro.codec.compression_level",
+                    Value::Bytes(vec![settings.compression_level]),
+                );
+            }
+            #[cfg(feature = "zstandard")]
+            Codec::Zstandard(settings) => {
+                metadata.insert(
+                    "avro.codec.compression_level",
+                    Value::Bytes(vec![settings.compression_level]),
+                );
+            }
+            _ => {}
+        }
 
         for (k, v) in &self.user_metadata {
             metadata.insert(k.as_str(), v.clone());
diff --git a/lang/rust/avro/tests/codecs.rs b/lang/rust/avro/tests/codecs.rs
new file mode 100644
index 000000000..5017d3389
--- /dev/null
+++ b/lang/rust/avro/tests/codecs.rs
@@ -0,0 +1,89 @@
+// 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.
+
+use apache_avro::{
+    types::{Record, Value},
+    Codec, Reader, Schema, Writer,
+};
+use apache_avro_test_helper::TestResult;
+
+#[test]
+fn avro_4032_null_codec_settings() -> TestResult {
+    avro_4032_codec_settings(Codec::Null)
+}
+#[test]
+fn avro_4032_deflate_codec_settings() -> TestResult {
+    avro_4032_codec_settings(Codec::Deflate)
+}
+
+#[test]
+#[cfg(feature = "bzip")]
+fn avro_4032_bzip_codec_settings() -> TestResult {
+    use apache_avro::Bzip2Settings;
+    use bzip2::Compression;
+    let codec = Codec::Bzip2(Bzip2Settings::new(Compression::fast().level() as 
u8));
+    avro_4032_codec_settings(codec)
+}
+
+#[test]
+#[cfg(feature = "xz")]
+fn avro_4032_xz_codec_settings() -> TestResult {
+    use apache_avro::XzSettings;
+    let codec = Codec::Xz(XzSettings::new(8));
+    avro_4032_codec_settings(codec)
+}
+
+#[test]
+#[cfg(feature = "zstandard")]
+fn avro_4032_zstandard_codec_settings() -> TestResult {
+    use apache_avro::ZstandardSettings;
+    let compression_level = 13;
+    let codec = Codec::Zstandard(ZstandardSettings::new(compression_level));
+    avro_4032_codec_settings(codec)
+}
+
+fn avro_4032_codec_settings(codec: Codec) -> TestResult {
+    let schema = Schema::parse_str(
+        r#"
+        {
+            "type": "record",
+            "name": "Test",
+            "fields": [
+                {"name": "f1", "type": "int"},
+                {"name": "f2", "type": "string"}
+            ]
+        }"#,
+    )?;
+
+    let mut writer = Writer::with_codec(&schema, Vec::new(), codec);
+    let mut record = Record::new(writer.schema()).unwrap();
+    record.put("f1", 27_i32);
+    record.put("f2", "foo");
+    writer.append(record)?;
+    let input = writer.into_inner()?;
+    let mut reader = Reader::new(&input[..])?;
+    assert_eq!(
+        reader.next().unwrap()?,
+        Value::Record(vec![
+            ("f1".to_string(), Value::Int(27)),
+            ("f2".to_string(), Value::String("foo".to_string())),
+        ])
+    );
+    assert!(reader.next().is_none());
+
+    Ok(())
+}

Reply via email to