This is an automated email from the ASF dual-hosted git repository.
mgrigorov pushed a commit to branch branch-1.11
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/branch-1.11 by this push:
new c2cc66a AVRO-3405: Add API to write/read user metadata in .avro file
(#1551)
c2cc66a is described below
commit c2cc66aa84476139bd8629dda59e55dfac2f58fc
Author: Martin Grigorov <[email protected]>
AuthorDate: Thu Feb 17 22:37:31 2022 +0200
AVRO-3405: Add API to write/read user metadata in .avro file (#1551)
* AVRO-3405 Extract duplicate code in a method
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405 Add API to read/write user metadata in .avro files
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405 Use 'user_metadata' consistently in the API
Simplify big method by splitting it into several ones.
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Inline few trivial getters
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Store user metadata as String->Bytes tuple
Fixes issues 1) and 2) from feedback at
https://github.com/apache/avro/pull/1551#pullrequestreview-885412252
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Enable builder API for Writer::user_meta_data
Add a unit test that could be used as an example how to pass the user
metadata to the builder API.
Fixes issue 3) from feedback at
https://github.com/apache/avro/pull/1551#pullrequestreview-885412252
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Test user metadata in Rust SDK interop tests
TODO: add this test to the other SDKs too
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Fix an obsolete warning message
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Fix formatting
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Add read/write interop tests for user metadata to Perl & Java
SDKs too
Now Rust, Perl & Java interop tests assert the values of 'stringKey' and
'bytesKey' user metadata.
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Simplify the assertions of user metadata in Perl interop test
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
* AVRO-3405: Return an error when trying to add user metadata with a key
'avro.xyz'
Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
(cherry picked from commit f888458721ce2573a6e04113a69a30ce785f8891)
---
.../main/java/org/apache/avro/util/RandomData.java | 2 +
.../java/org/apache/avro/DataFileInteropTest.java | 24 +++-
lang/perl/share/interop-data-generate | 6 +
lang/perl/xt/interop.t | 15 ++-
lang/rust/examples/generate_interop_data.rs | 9 ++
lang/rust/examples/test_interop_data.rs | 24 +++-
lang/rust/src/error.rs | 6 +
lang/rust/src/reader.rs | 129 +++++++++++++++----
lang/rust/src/writer.rs | 141 ++++++++++++++++++---
9 files changed, 306 insertions(+), 50 deletions(-)
diff --git a/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
b/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
index 8806920..12b8a7b 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/util/RandomData.java
@@ -170,6 +170,8 @@ public class RandomData implements Iterable<Object> {
Schema sch = new Schema.Parser().parse(new File(args[0]));
try (DataFileWriter<Object> writer = new DataFileWriter<>(new
GenericDatumWriter<>())) {
writer.setCodec(CodecFactory.fromString(args.length >= 4 ? args[3] :
"null"));
+ writer.setMeta("stringKey", "stringValue");
+ writer.setMeta("bytesKey",
"bytesValue".getBytes(StandardCharsets.UTF_8));
writer.create(sch, new File(args[1]));
for (Object datum : new RandomData(sch, Integer.parseInt(args[2]))) {
diff --git
a/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
b/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
index 6093015..7828532 100644
--- a/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
+++ b/lang/java/ipc/src/test/java/org/apache/avro/DataFileInteropTest.java
@@ -17,16 +17,20 @@
*/
package org.apache.avro;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.apache.avro.file.DataFileReader;
-import org.apache.avro.file.FileReader;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.io.DatumReader;
import org.apache.avro.specific.SpecificDatumReader;
-import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -79,19 +83,27 @@ public class DataFileInteropTest {
private <T extends Object> void readFiles(DatumReaderProvider<T> provider)
throws IOException {
for (File f : Objects.requireNonNull(DATAFILE_DIR.listFiles())) {
System.out.println("Reading: " + f.getName());
- try (FileReader<? extends Object> reader = DataFileReader.openReader(f,
provider.get())) {
+ try (DataFileReader<? extends Object> reader = (DataFileReader<? extends
Object>) DataFileReader.openReader(f,
+ provider.get())) {
+
+ // Ignore avro.schema & avro.codec. Some SDKs do not support user
metadata.
+ if (reader.getMetaKeys().size() > 2) {
+ assertEquals("stringValue", reader.getMetaString("stringKey"));
+ assertArrayEquals("bytesValue".getBytes(StandardCharsets.UTF_8),
reader.getMeta("bytesKey"));
+ }
+
int i = 0;
for (Object datum : reader) {
i++;
- Assert.assertNotNull(datum);
+ assertNotNull(datum);
}
- Assert.assertNotEquals(0, i);
+ assertNotEquals(0, i);
}
}
}
interface DatumReaderProvider<T extends Object> {
- public DatumReader<T> get();
+ DatumReader<T> get();
}
}
diff --git a/lang/perl/share/interop-data-generate
b/lang/perl/share/interop-data-generate
index c659605..5d2cd79 100644
--- a/lang/perl/share/interop-data-generate
+++ b/lang/perl/share/interop-data-generate
@@ -54,6 +54,11 @@ my $datum = {
},
};
+my $metadata = {
+ stringKey => 'stringValue',
+ bytesKey => 'bytesValue'
+};
+
while (my ($codec, $enabled) = each(%Avro::DataFile::ValidCodec)) {
next unless $enabled;
my $outdir = '../../build/interop/data';
@@ -65,6 +70,7 @@ while (my ($codec, $enabled) =
each(%Avro::DataFile::ValidCodec)) {
my $writer = Avro::DataFileWriter->new(
fh => $fh,
codec => $codec,
+ metadata => $metadata,
writer_schema => $writer_schema
);
$writer->print($datum);
diff --git a/lang/perl/xt/interop.t b/lang/perl/xt/interop.t
index 9af1648..d50ca6c 100644
--- a/lang/perl/xt/interop.t
+++ b/lang/perl/xt/interop.t
@@ -24,6 +24,11 @@ use IO::File;
use_ok 'Avro::DataFile';
use_ok 'Avro::DataFileReader';
+my $expected_metadata = {
+ stringKey => 'stringValue',
+ bytesKey => 'bytesValue'
+};
+
for my $path (glob '../../build/interop/data/*.avro') {
my $fn = basename($path);
substr($fn, rindex $fn, '.') = '';
@@ -36,7 +41,15 @@ for my $path (glob '../../build/interop/data/*.avro') {
}
}
my $fh = IO::File->new($path);
- Avro::DataFileReader->new(fh => $fh);
+ my $reader = Avro::DataFileReader->new(fh => $fh);
+
+ my $metadata = $reader->metadata;
+ if (exists $metadata->{stringKey}) {
+ is($metadata->{stringKey}, $expected_metadata->{stringKey}, "check
user metadata: stringKey ");
+ }
+ if (exists $metadata->{bytesKey}) {
+ is($metadata->{bytesKey}, join('', $expected_metadata->{bytesKey}),
"check user metadata: bytesKey ");
+ }
diag("Succeeded: ${path}");
}
diff --git a/lang/rust/examples/generate_interop_data.rs
b/lang/rust/examples/generate_interop_data.rs
index 514ee77..3d91140 100644
--- a/lang/rust/examples/generate_interop_data.rs
+++ b/lang/rust/examples/generate_interop_data.rs
@@ -78,6 +78,8 @@ fn main() -> anyhow::Result<()> {
for codec in Codec::iter() {
let mut writer = Writer::with_codec(&schema, Vec::new(), codec);
+ write_user_metadata(&mut writer)?;
+
let datum = create_datum(&schema);
writer.append(datum)?;
let bytes = writer.into_inner()?;
@@ -97,3 +99,10 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
+
+fn write_user_metadata(writer: &mut Writer<Vec<u8>>) -> anyhow::Result<()> {
+ writer.add_user_metadata("stringKey".to_string(), "stringValue")?;
+ writer.add_user_metadata("bytesKey".to_string(), b"bytesValue")?;
+
+ Ok(())
+}
diff --git a/lang/rust/examples/test_interop_data.rs
b/lang/rust/examples/test_interop_data.rs
index e04020e..85d59de 100644
--- a/lang/rust/examples/test_interop_data.rs
+++ b/lang/rust/examples/test_interop_data.rs
@@ -16,9 +16,11 @@
// under the License.
use apache_avro::Reader;
-use std::ffi::OsStr;
+use std::{collections::HashMap, ffi::OsStr, fs::File};
fn main() -> anyhow::Result<()> {
+ let expected_user_metadata: HashMap<String, Vec<u8>> =
create_expected_user_metadata();
+
let data_dir = std::fs::read_dir("../../build/interop/data/")
.expect("Unable to list the interop data directory");
@@ -36,6 +38,9 @@ fn main() -> anyhow::Result<()> {
println!("Checking {:?}", &path);
let content = std::fs::File::open(&path)?;
let reader = Reader::new(&content)?;
+
+ test_user_metadata(&reader, &expected_user_metadata);
+
for value in reader {
if let Err(e) = value {
errors.push(format!(
@@ -57,3 +62,20 @@ fn main() -> anyhow::Result<()> {
);
}
}
+
+fn create_expected_user_metadata() -> HashMap<String, Vec<u8>> {
+ let mut user_metadata: HashMap<String, Vec<u8>> = HashMap::new();
+ user_metadata.insert(
+ "stringKey".to_string(),
+ "stringValue".to_string().into_bytes(),
+ );
+ user_metadata.insert("bytesKey".to_string(), b"bytesValue".to_vec());
+ user_metadata
+}
+
+fn test_user_metadata(reader: &Reader<&File>, expected_user_metadata:
&HashMap<String, Vec<u8>>) {
+ let user_metadata = reader.user_metadata();
+ if !user_metadata.is_empty() {
+ assert_eq!(user_metadata, expected_user_metadata);
+ }
+}
diff --git a/lang/rust/src/error.rs b/lang/rust/src/error.rs
index d687eea..1ed2f38 100644
--- a/lang/rust/src/error.rs
+++ b/lang/rust/src/error.rs
@@ -376,6 +376,12 @@ pub enum Error {
/// Error while resolving Schema::Ref
#[error("Unresolved schema reference: {0}")]
SchemaResolutionError(String),
+
+ #[error("The file metadata is already flushed.")]
+ FileHeaderAlreadyWritten,
+
+ #[error("Metadata keys starting with 'avro.' are reserved for internal
usage: {0}.")]
+ InvalidMetadataKey(String),
}
impl serde::ser::Error for Error {
diff --git a/lang/rust/src/reader.rs b/lang/rust/src/reader.rs
index d46b3bd..e4bf706 100644
--- a/lang/rust/src/reader.rs
+++ b/lang/rust/src/reader.rs
@@ -19,6 +19,7 @@
use crate::{decode::decode, schema::Schema, types::Value, util, AvroResult,
Codec, Error};
use serde_json::from_slice;
use std::{
+ collections::HashMap,
io::{ErrorKind, Read},
str::FromStr,
};
@@ -35,6 +36,7 @@ struct Block<R> {
marker: [u8; 16],
codec: Codec,
writer_schema: Schema,
+ user_metadata: HashMap<String, Vec<u8>>,
}
impl<R: Read> Block<R> {
@@ -47,6 +49,7 @@ impl<R: Read> Block<R> {
buf_idx: 0,
message_count: 0,
marker: [0; 16],
+ user_metadata: Default::default(),
};
block.read_header()?;
@@ -67,32 +70,18 @@ impl<R: Read> Block<R> {
return Err(Error::HeaderMagic);
}
- if let Value::Map(meta) = decode(&meta_schema, &mut self.reader)? {
- // TODO: surface original parse schema errors instead of
coalescing them here
- let json = meta
- .get("avro.schema")
- .and_then(|bytes| {
- if let Value::Bytes(ref bytes) = *bytes {
- from_slice(bytes.as_ref()).ok()
- } else {
- None
- }
- })
- .ok_or(Error::GetAvroSchemaFromMap)?;
- self.writer_schema = Schema::parse(&json)?;
-
- if let Some(codec) = meta
- .get("avro.codec")
- .and_then(|codec| {
- if let Value::Bytes(ref bytes) = *codec {
- std::str::from_utf8(bytes.as_ref()).ok()
- } else {
- None
- }
- })
- .and_then(|codec| Codec::from_str(codec).ok())
- {
- self.codec = codec;
+ if let Value::Map(metadata) = decode(&meta_schema, &mut self.reader)? {
+ self.read_writer_schema(&metadata)?;
+ self.read_codec(&metadata)?;
+
+ for (key, value) in metadata {
+ if key == "avro.schema" || key == "avro.codec" {
+ // already processed
+ } else if key.starts_with("avro.") {
+ warn!("Ignoring unknown metadata key: {}", key);
+ } else {
+ self.read_user_metadata(key, value)?;
+ }
}
} else {
return Err(Error::GetHeaderMetadata);
@@ -184,6 +173,54 @@ impl<R: Read> Block<R> {
self.message_count -= 1;
Ok(Some(item))
}
+
+ fn read_writer_schema(&mut self, metadata: &HashMap<String, Value>) ->
AvroResult<()> {
+ let json = metadata
+ .get("avro.schema")
+ .and_then(|bytes| {
+ if let Value::Bytes(ref bytes) = *bytes {
+ from_slice(bytes.as_ref()).ok()
+ } else {
+ None
+ }
+ })
+ .ok_or(Error::GetAvroSchemaFromMap)?;
+ self.writer_schema = Schema::parse(&json)?;
+ Ok(())
+ }
+
+ fn read_codec(&mut self, metadata: &HashMap<String, Value>) ->
AvroResult<()> {
+ if let Some(codec) = metadata
+ .get("avro.codec")
+ .and_then(|codec| {
+ if let Value::Bytes(ref bytes) = *codec {
+ std::str::from_utf8(bytes.as_ref()).ok()
+ } else {
+ None
+ }
+ })
+ .and_then(|codec| Codec::from_str(codec).ok())
+ {
+ self.codec = codec;
+ }
+ Ok(())
+ }
+
+ fn read_user_metadata(&mut self, key: String, value: Value) ->
AvroResult<()> {
+ match value {
+ Value::Bytes(ref vec) => {
+ self.user_metadata.insert(key, vec.clone());
+ Ok(())
+ }
+ wrong => {
+ warn!(
+ "User metadata values must be Value::Bytes, found {:?}",
+ wrong
+ );
+ Ok(())
+ }
+ }
+ }
}
/// Main interface for reading Avro formatted values.
@@ -242,15 +279,23 @@ impl<'a, R: Read> Reader<'a, R> {
}
/// Get a reference to the writer `Schema`.
+ #[inline]
pub fn writer_schema(&self) -> &Schema {
&self.block.writer_schema
}
/// Get a reference to the optional reader `Schema`.
+ #[inline]
pub fn reader_schema(&self) -> Option<&Schema> {
self.reader_schema
}
+ /// Get a reference to the user metadata
+ #[inline]
+ pub fn user_metadata(&self) -> &HashMap<String, Vec<u8>> {
+ &self.block.user_metadata
+ }
+
#[inline]
fn read_next(&mut self) -> AvroResult<Option<Value>> {
let read_schema = if self.should_resolve_schema {
@@ -499,4 +544,36 @@ mod tests {
assert!(value.is_err());
}
}
+
+ #[test]
+ fn test_avro_3405_read_user_metadata_success() {
+ use crate::writer::Writer;
+
+ let schema = Schema::parse_str(SCHEMA).unwrap();
+ let mut writer = Writer::new(&schema, Vec::new());
+
+ let mut user_meta_data: HashMap<String, Vec<u8>> = HashMap::new();
+ user_meta_data.insert(
+ "stringKey".to_string(),
+ "stringValue".to_string().into_bytes(),
+ );
+ user_meta_data.insert("bytesKey".to_string(), b"bytesValue".to_vec());
+ user_meta_data.insert("vecKey".to_string(), vec![1, 2, 3]);
+
+ for (k, v) in user_meta_data.iter() {
+ writer.add_user_metadata(k.to_string(), v).unwrap();
+ }
+
+ let mut record = Record::new(&schema).unwrap();
+ record.put("a", 27i64);
+ record.put("b", "foo");
+
+ writer.append(record.clone()).unwrap();
+ writer.append(record.clone()).unwrap();
+ writer.flush().unwrap();
+ let result = writer.into_inner().unwrap();
+
+ let reader = Reader::new(&result[..]).unwrap();
+ assert_eq!(reader.user_metadata(), &user_meta_data);
+ }
}
diff --git a/lang/rust/src/writer.rs b/lang/rust/src/writer.rs
index a222a0f..f8a4554 100644
--- a/lang/rust/src/writer.rs
+++ b/lang/rust/src/writer.rs
@@ -49,6 +49,8 @@ pub struct Writer<'a, W> {
marker: Vec<u8>,
#[builder(default = false, setter(skip))]
has_header: bool,
+ #[builder(default)]
+ user_metadata: HashMap<String, Value>,
}
impl<'a, W: Write> Writer<'a, W> {
@@ -83,14 +85,7 @@ impl<'a, W: Write> Writer<'a, W> {
/// internal buffering for performance reasons. If you want to be sure the
value has been
/// written, then call [`flush`](struct.Writer.html#method.flush).
pub fn append<T: Into<Value>>(&mut self, value: T) -> AvroResult<usize> {
- let n = if !self.has_header {
- let header = self.header()?;
- let n = self.append_bytes(header.as_ref())?;
- self.has_header = true;
- n
- } else {
- 0
- };
+ let n = self.maybe_write_header()?;
let avro = value.into();
write_value_ref(self.schema, &avro, &mut self.buffer)?;
@@ -112,14 +107,7 @@ impl<'a, W: Write> Writer<'a, W> {
/// internal buffering for performance reasons. If you want to be sure the
value has been
/// written, then call [`flush`](struct.Writer.html#method.flush).
pub fn append_value_ref(&mut self, value: &Value) -> AvroResult<usize> {
- let n = if !self.has_header {
- let header = self.header()?;
- let n = self.append_bytes(header.as_ref())?;
- self.has_header = true;
- n
- } else {
- 0
- };
+ let n = self.maybe_write_header()?;
write_value_ref(self.schema, value, &mut self.buffer)?;
@@ -286,6 +274,21 @@ impl<'a, W: Write> Writer<'a, W> {
self.writer.write(bytes).map_err(Error::WriteBytes)
}
+ /// Adds custom metadata to the file.
+ /// This method could be used only before adding the first record to the
writer.
+ pub fn add_user_metadata<T: AsRef<[u8]>>(&mut self, key: String, value: T)
-> AvroResult<()> {
+ if !self.has_header {
+ if key.starts_with("avro.") {
+ return Err(Error::InvalidMetadataKey(key));
+ }
+ self.user_metadata
+ .insert(key, Value::Bytes(value.as_ref().to_vec()));
+ Ok(())
+ } else {
+ Err(Error::FileHeaderAlreadyWritten)
+ }
+ }
+
/// Create an Avro header based on schema, codec and sync marker.
fn header(&self) -> Result<Vec<u8>, Error> {
let schema_bytes = serde_json::to_string(self.schema)
@@ -296,6 +299,10 @@ impl<'a, W: Write> Writer<'a, W> {
metadata.insert("avro.schema", Value::Bytes(schema_bytes));
metadata.insert("avro.codec", self.codec.into());
+ for (k, v) in &self.user_metadata {
+ metadata.insert(k.as_str(), v.clone());
+ }
+
let mut header = Vec::new();
header.extend_from_slice(AVRO_OBJECT_HEADER);
encode(
@@ -307,6 +314,17 @@ impl<'a, W: Write> Writer<'a, W> {
Ok(header)
}
+
+ fn maybe_write_header(&mut self) -> AvroResult<usize> {
+ if !self.has_header {
+ let header = self.header()?;
+ let n = self.append_bytes(header.as_ref())?;
+ self.has_header = true;
+ Ok(n)
+ } else {
+ Ok(0)
+ }
+ }
}
/// Encode a compatible value (implementing the `ToAvro` trait) into Avro
format, also performing
@@ -812,4 +830,95 @@ mod tests {
data.as_slice()
);
}
+
+ #[test]
+ fn test_avro_3405_writer_add_metadata_success() {
+ let schema = Schema::parse_str(SCHEMA).unwrap();
+ let mut writer = Writer::new(&schema, Vec::new());
+
+ writer
+ .add_user_metadata("stringKey".to_string(),
"stringValue".to_string())
+ .unwrap();
+ writer
+ .add_user_metadata("strKey".to_string(), "strValue")
+ .unwrap();
+ writer
+ .add_user_metadata("bytesKey".to_string(), b"bytesValue")
+ .unwrap();
+ writer
+ .add_user_metadata("vecKey".to_string(), vec![1, 2, 3])
+ .unwrap();
+
+ let mut record = Record::new(&schema).unwrap();
+ record.put("a", 27i64);
+ record.put("b", "foo");
+
+ writer.append(record.clone()).unwrap();
+ writer.append(record.clone()).unwrap();
+ writer.flush().unwrap();
+ let result = writer.into_inner().unwrap();
+
+ assert_eq!(result.len(), 260);
+ }
+
+ #[test]
+ fn test_avro_3405_writer_add_metadata_failure() {
+ let schema = Schema::parse_str(SCHEMA).unwrap();
+ let mut writer = Writer::new(&schema, Vec::new());
+
+ let mut record = Record::new(&schema).unwrap();
+ record.put("a", 27i64);
+ record.put("b", "foo");
+ writer.append(record.clone()).unwrap();
+
+ match writer.add_user_metadata("stringKey".to_string(),
"value2".to_string()) {
+ Err(e @ Error::FileHeaderAlreadyWritten) => {
+ assert_eq!(e.to_string(), "The file metadata is already
flushed.")
+ }
+ Err(e) => panic!(
+ "Unexpected error occurred while writing user metadata: {:?}",
+ e
+ ),
+ Ok(_) => panic!("Expected an error that metadata cannot be added
after adding data"),
+ }
+ }
+
+ #[test]
+ fn test_avro_3405_writer_add_metadata_reserved_prefix_failure() {
+ let schema = Schema::parse_str(SCHEMA).unwrap();
+ let mut writer = Writer::new(&schema, Vec::new());
+
+ let key = "avro.stringKey".to_string();
+ match writer.add_user_metadata(key.clone(), "value") {
+ Err(ref e @ Error::InvalidMetadataKey(_)) => {
+ assert_eq!(e.to_string(), format!("Metadata keys starting with
'avro.' are reserved for internal usage: {}.", key))
+ }
+ Err(e) => panic!(
+ "Unexpected error occurred while writing user metadata with
reserved prefix ('avro.'): {:?}",
+ e
+ ),
+ Ok(_) => panic!("Expected an error that the metadata key cannot be
prefixed with 'avro.'"),
+ }
+ }
+
+ #[test]
+ fn test_avro_3405_writer_add_metadata_with_builder_api_success() {
+ let schema = Schema::parse_str(SCHEMA).unwrap();
+
+ let mut user_meta_data: HashMap<String, Value> = HashMap::new();
+ user_meta_data.insert(
+ "stringKey".to_string(),
+ Value::String("stringValue".to_string()),
+ );
+ user_meta_data.insert("bytesKey".to_string(),
Value::Bytes(b"bytesValue".to_vec()));
+ user_meta_data.insert("vecKey".to_string(), Value::Bytes(vec![1, 2,
3]));
+
+ let writer: Writer<'_, Vec<u8>> = Writer::builder()
+ .writer(Vec::new())
+ .schema(&schema)
+ .user_metadata(user_meta_data.clone())
+ .build();
+
+ assert_eq!(writer.user_metadata, user_meta_data);
+ }
}