Repository: apache/arrow-rs
Pull request #5779 (2024-05-17 03:01:12)

Support casting a `FixedSizedList<T>[1]` to `T`
# Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes #123` indicates that this PR will close issue #123.
-->

N/A
This is a followup to #5340 

# Rationale for this change
 
<!--
Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes.
-->

This implements the mirror of #5340 -- casting `FixedSizedList<T>[1]` to `T`. Taken together, this allows `FixedSizedList<T>[1]` to be used "as though it were a `T`" for the purpose of casting.

# What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR.
-->
* New casting method: `cast_single_element_fixed_size_list_to_values`
* Minor bug fix in `FixedSizeListArray::try_new`: allow `nulls` to be `None` when `values` has null buffer with null_count of 0.

# Are there any user-facing changes?


<!--
If there are user-facing changes then we may require documentation to be updated before approving the PR.
-->

<!---
If there are any breaking changes to public APIs, please add the `breaking change` label.
-->



Patch:
diff --git a/arrow-array/src/array/fixed_size_list_array.rs b/arrow-array/src/array/fixed_size_list_array.rs
index 1d07daf4c..7bb4e0c5e 100644
--- a/arrow-array/src/array/fixed_size_list_array.rs
+++ b/arrow-array/src/array/fixed_size_list_array.rs
@@ -183,7 +183,8 @@ impl FixedSizeListArray {
                 || nulls
                     .as_ref()
                     .map(|n| n.expand(size as _).contains(&a))
-                    .unwrap_or_default();
+                    .unwrap_or_default()
+                || (nulls.is_none() && a.null_count() == 0);
 
             if !nulls_valid {
                 return Err(ArrowError::InvalidArgumentError(format!(
diff --git a/arrow-cast/src/cast/list.rs b/arrow-cast/src/cast/list.rs
index 2ceab1d0c..ec7a5c57d 100644
--- a/arrow-cast/src/cast/list.rs
+++ b/arrow-cast/src/cast/list.rs
@@ -41,6 +41,15 @@ pub(crate) fn cast_values_to_fixed_size_list(
     Ok(Arc::new(list))
 }
 
+pub(crate) fn cast_single_element_fixed_size_list_to_values(
+    array: &dyn Array,
+    to: &DataType,
+    cast_options: &CastOptions,
+) -> Result<ArrayRef, ArrowError> {
+    let values = array.as_fixed_size_list().values();
+    cast_with_options(values, to, cast_options)
+}
+
 pub(crate) fn cast_fixed_size_list_to_list<OffsetSize>(
     array: &dyn Array,
 ) -> Result<ArrayRef, ArrowError>
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index ccbc8727f..f2fdfd044 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -157,6 +157,8 @@ pub fn can_cast_types(from_type: &DataType, to_type: &DataType) -> bool {
         (_, LargeList(list_to)) => can_cast_types(from_type, list_to.data_type()),
         (_, FixedSizeList(list_to,size)) if *size == 1 => {
             can_cast_types(from_type, list_to.data_type())},
+        (FixedSizeList(list_from,size), _) if *size == 1 => {
+            can_cast_types(list_from.data_type(), to_type)},
         // cast one decimal type to another decimal type
         (Decimal128(_, _), Decimal128(_, _)) => true,
         (Decimal256(_, _), Decimal256(_, _)) => true,
@@ -712,6 +714,9 @@ pub fn cast_with_options(
         (_, FixedSizeList(ref to, size)) if *size == 1 => {
             cast_values_to_fixed_size_list(array, to, *size, cast_options)
         }
+        (FixedSizeList(_, size), _) if *size == 1 => {
+            cast_single_element_fixed_size_list_to_values(array, to_type, cast_options)
+        }
         (Decimal128(_, s1), Decimal128(p2, s2)) => {
             cast_decimal_to_decimal_same_type::<Decimal128Type>(
                 array.as_primitive(),
@@ -6895,6 +6900,85 @@ mod tests {
         assert_eq!(&expect.value(0), &actual.value(0));
     }
 
+    #[test]
+    fn test_cast_single_element_fixed_size_list() {
+        // FixedSizeList<T>[1] => T
+        let from_array = Arc::new(FixedSizeListArray::from_iter_primitive::<Int16Type, _, _>(
+            [(Some([Some(5)]))],
+            1,
+        )) as ArrayRef;
+        let casted_array = cast(&from_array, &DataType::Int32).unwrap();
+        let actual: &Int32Array = casted_array.as_primitive();
+        let expected = Int32Array::from(vec![Some(5)]);
+        assert_eq!(&expected, actual);
+
+        // FixedSizeList<T>[1] => FixedSizeList<U>[1]
+        let from_array = Arc::new(FixedSizeListArray::from_iter_primitive::<Int16Type, _, _>(
+            [(Some([Some(5)]))],
+            1,
+        )) as ArrayRef;
+        let to_field = Arc::new(Field::new("dummy", DataType::Float32, false));
+        let actual = cast(&from_array, &DataType::FixedSizeList(to_field.clone(), 1)).unwrap();
+        let expected = Arc::new(FixedSizeListArray::new(
+            to_field.clone(),
+            1,
+            Arc::new(Float32Array::from(vec![Some(5.0)])) as ArrayRef,
+            None,
+        )) as ArrayRef;
+        assert_eq!(*expected, *actual);
+
+        // FixedSizeList<T>[1] => FixedSizeList<FixdSizedList<U>[1]>[1]
+        let from_array = Arc::new(FixedSizeListArray::from_iter_primitive::<Int16Type, _, _>(
+            [(Some([Some(5)]))],
+            1,
+        )) as ArrayRef;
+        let to_field_inner = Arc::new(Field::new("item", DataType::Float32, false));
+        let to_field = Arc::new(Field::new(
+            "dummy",
+            DataType::FixedSizeList(to_field_inner.clone(), 1),
+            false,
+        ));
+        let actual = cast(&from_array, &DataType::FixedSizeList(to_field.clone(), 1)).unwrap();
+        let expected = Arc::new(FixedSizeListArray::new(
+            to_field.clone(),
+            1,
+            Arc::new(FixedSizeListArray::new(
+                to_field_inner.clone(),
+                1,
+                Arc::new(Float32Array::from(vec![Some(5.0)])) as ArrayRef,
+                None,
+            )) as ArrayRef,
+            None,
+        )) as ArrayRef;
+        assert_eq!(*expected, *actual);
+
+        // T => FixedSizeList<T>[1] (non-nullable)
+        let field = Arc::new(Field::new("dummy", DataType::Float32, false));
+        let from_array = Arc::new(Int8Array::from(vec![Some(5)])) as ArrayRef;
+        let casted_array = cast(&from_array, &DataType::FixedSizeList(field.clone(), 1)).unwrap();
+        let actual = casted_array.as_fixed_size_list();
+        let expected = Arc::new(FixedSizeListArray::new(
+            field.clone(),
+            1,
+            Arc::new(Float32Array::from(vec![Some(5.0)])) as ArrayRef,
+            None,
+        )) as ArrayRef;
+        assert_eq!(expected.as_ref(), actual);
+
+        // T => FixedSizeList<T>[1] (nullable)
+        let field = Arc::new(Field::new("nullable", DataType::Float32, true));
+        let from_array = Arc::new(Int8Array::from(vec![None])) as ArrayRef;
+        let casted_array = cast(&from_array, &DataType::FixedSizeList(field.clone(), 1)).unwrap();
+        let actual = casted_array.as_fixed_size_list();
+        let expected = Arc::new(FixedSizeListArray::new(
+            field.clone(),
+            1,
+            Arc::new(Float32Array::from(vec![None])) as ArrayRef,
+            None,
+        )) as ArrayRef;
+        assert_eq!(expected.as_ref(), actual);
+    }
+
     #[test]
     fn test_cast_list_containers() {
         // large-list to list
diff --git a/object_store/Cargo.toml b/object_store/Cargo.toml
index 20d7c2d61..62cbce79f 100644
--- a/object_store/Cargo.toml
+++ b/object_store/Cargo.toml
@@ -35,7 +35,7 @@ bytes = "1.0"
 chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
 futures = "0.3"
 humantime = "2.1"
-itertools = "0.12.0"
+itertools = "0.13.0"
 parking_lot = { version = "0.12" }
 percent-encoding = "2.1"
 snafu = "0.7"
diff --git a/parquet/src/file/footer.rs b/parquet/src/file/footer.rs
index 9695dbeae..7a75576c3 100644
--- a/parquet/src/file/footer.rs
+++ b/parquet/src/file/footer.rs
@@ -27,14 +27,30 @@ use crate::file::{metadata::*, reader::ChunkReader, FOOTER_SIZE, PARQUET_MAGIC};
 
 use crate::schema::types::{self, SchemaDescriptor};
 
-/// Layout of Parquet file
+/// Reads the [ParquetMetaData] from the footer of the parquet file.
+///
+/// # Layout of Parquet file
+/// ```text
 /// +---------------------------+-----+---+
 /// |      Rest of file         |  B  | A |
 /// +---------------------------+-----+---+
-/// where A: parquet footer, B: parquet metadata.
+/// ```
+/// where
+/// * `A`: parquet footer which stores the length of the metadata.
+/// * `B`: parquet metadata.
+///
+/// # I/O
+///
+/// This method first reads the last 8 bytes of the file via
+/// [`ChunkReader::get_read`] to get the the parquet footer which contains the
+/// metadata length.
+///
+/// It then issues a second `get_read` to read the encoded metadata
+/// metadata.
 ///
-/// The reader first reads DEFAULT_FOOTER_SIZE bytes from the end of the file.
-/// If it is not enough according to the length indicated in the footer, it reads more bytes.
+/// # See Also
+/// [`decode_metadata`] for decoding the metadata from the bytes.
+/// [`decode_footer`] for decoding the metadata length from the footer.
 pub fn parse_metadata<R: ChunkReader>(chunk_reader: &R) -> Result<ParquetMetaData> {
     // check file is large enough to hold footer
     let file_size = chunk_reader.len();
@@ -65,7 +81,13 @@ pub fn parse_metadata<R: ChunkReader>(chunk_reader: &R) -> Result<ParquetMetaDat
     decode_metadata(chunk_reader.get_bytes(start, metadata_len)?.as_ref())
 }
 
-/// Decodes [`ParquetMetaData`] from the provided bytes
+/// Decodes [`ParquetMetaData`] from the provided bytes.
+///
+/// Typically this is used to decode the metadata from the end of a parquet
+/// file. The format of `buf` is the Thift compact binary protocol, as specified
+/// by the [Parquet Spec].
+///
+/// [Parquet Spec]: https://github.com/apache/parquet-format#metadata
 pub fn decode_metadata(buf: &[u8]) -> Result<ParquetMetaData> {
     // TODO: row group filtering
     let mut prot = TCompactSliceInputProtocol::new(buf);
@@ -90,7 +112,17 @@ pub fn decode_metadata(buf: &[u8]) -> Result<ParquetMetaData> {
     Ok(ParquetMetaData::new(file_metadata, row_groups))
 }
 
-/// Decodes the footer returning the metadata length in bytes
+/// Decodes the Parquet footer returning the metadata length in bytes
+///
+/// A parquet footer is 8 bytes long and has the following layout:
+/// * 4 bytes for the metadata length
+/// * 4 bytes for the magic bytes 'PAR1'
+///
+/// ```text
+/// +-----+--------+
+/// | len | 'PAR1' |
+/// +-----+--------+
+/// ```
 pub fn decode_footer(slice: &[u8; FOOTER_SIZE]) -> Result<usize> {
     // check this is indeed a parquet file
     if slice[4..] != PARQUET_MAGIC {
diff --git a/parquet_derive/src/parquet_field.rs b/parquet_derive/src/parquet_field.rs
index 3ab85a897..9fff76c42 100644
--- a/parquet_derive/src/parquet_field.rs
+++ b/parquet_derive/src/parquet_field.rs
@@ -136,7 +136,7 @@ impl Field {
                 Type::Option(_) => unimplemented!("Unsupported nesting encountered"),
                 Type::Reference(_, ref second_type)
                 | Type::Vec(ref second_type)
-                | Type::Array(ref second_type)
+                | Type::Array(ref second_type, _)
                 | Type::Slice(ref second_type) => match **second_type {
                     Type::TypePath(_) => Some(self.optional_definition_levels()),
                     _ => unimplemented!("Unsupported nesting encountered"),
@@ -144,11 +144,11 @@ impl Field {
             },
             Type::Reference(_, ref first_type)
             | Type::Vec(ref first_type)
-            | Type::Array(ref first_type)
+            | Type::Array(ref first_type, _)
             | Type::Slice(ref first_type) => match **first_type {
                 Type::TypePath(_) => None,
                 Type::Vec(ref second_type)
-                | Type::Array(ref second_type)
+                | Type::Array(ref second_type, _)
                 | Type::Slice(ref second_type) => match **second_type {
                     Type::TypePath(_) => None,
                     Type::Reference(_, ref third_type) => match **third_type {
@@ -161,7 +161,7 @@ impl Field {
                     match **second_type {
                         Type::TypePath(_) => Some(self.optional_definition_levels()),
                         Type::Vec(ref third_type)
-                        | Type::Array(ref third_type)
+                        | Type::Array(ref third_type, _)
                         | Type::Slice(ref third_type) => match **third_type {
                             Type::TypePath(_) => Some(self.optional_definition_levels()),
                             Type::Reference(_, ref fourth_type) => match **fourth_type {
@@ -316,25 +316,23 @@ impl Field {
         let logical_type = self.ty.logical_type();
         let repetition = self.ty.repetition();
         let converted_type = self.ty.converted_type();
+        let length = self.ty.length();
+
+        let mut builder = quote! {
+            ParquetType::primitive_type_builder(#field_name, #physical_type)
+                .with_logical_type(#logical_type)
+                .with_repetition(#repetition)
+        };
 
         if let Some(converted_type) = converted_type {
-            quote! {
-                fields.push(ParquetType::primitive_type_builder(#field_name, #physical_type)
-                    .with_logical_type(#logical_type)
-                    .with_repetition(#repetition)
-                    .with_converted_type(#converted_type)
-                    .build().unwrap().into()
-                )
-            }
-        } else {
-            quote! {
-                fields.push(ParquetType::primitive_type_builder(#field_name, #physical_type)
-                    .with_logical_type(#logical_type)
-                    .with_repetition(#repetition)
-                    .build().unwrap().into()
-                )
-            }
+            builder = quote! { #builder.with_converted_type(#converted_type) };
+        }
+
+        if let Some(length) = length {
+            builder = quote! { #builder.with_length(#length) };
         }
+
+        quote! {  fields.push(#builder.build().unwrap().into()) }
     }
 
     fn option_into_vals(&self) -> proc_macro2::TokenStream {
@@ -394,7 +392,7 @@ impl Field {
                 quote! { rec.#field_name.signed_duration_since(::chrono::NaiveDate::from_ymd(1970, 1, 1)).num_days() as i32 }
             }
             Some(ThirdPartyType::Uuid) => {
-                quote! { (&rec.#field_name.to_string()[..]).into() }
+                quote! { rec.#field_name.as_bytes().to_vec().into() }
             }
             _ => {
                 if self.is_a_byte_buf {
@@ -430,7 +428,7 @@ impl Field {
                 }
             }
             Some(ThirdPartyType::Uuid) => {
-                quote! { ::uuid::Uuid::parse_str(vals[i].data().convert()).unwrap() }
+                quote! { ::uuid::Uuid::from_bytes(vals[i].data().try_into().unwrap()) }
             }
             _ => match &self.ty {
                 Type::TypePath(_) => match self.ty.last_part().as_str() {
@@ -469,7 +467,7 @@ impl Field {
 #[allow(clippy::large_enum_variant)]
 #[derive(Debug, PartialEq)]
 enum Type {
-    Array(Box<Type>),
+    Array(Box<Type>, syn::Expr),
     Option(Box<Type>),
     Slice(Box<Type>),
     Vec(Box<Type>),
@@ -542,7 +540,7 @@ impl Type {
             Type::TypePath(_) => parent_ty.unwrap_or(ty),
             Type::Option(ref first_type)
             | Type::Vec(ref first_type)
-            | Type::Array(ref first_type)
+            | Type::Array(ref first_type, _)
             | Type::Slice(ref first_type)
             | Type::Reference(_, ref first_type) => {
                 Type::leaf_type_recursive_helper(first_type, Some(ty))
@@ -560,7 +558,7 @@ impl Type {
             Type::TypePath(ref type_) => type_,
             Type::Option(ref first_type)
             | Type::Vec(ref first_type)
-            | Type::Array(ref first_type)
+            | Type::Array(ref first_type, _)
             | Type::Slice(ref first_type)
             | Type::Reference(_, ref first_type) => match **first_type {
                 Type::TypePath(ref type_) => type_,
@@ -607,7 +605,7 @@ impl Type {
         let leaf_type = self.leaf_type_recursive();
 
         match leaf_type {
-            Type::Array(ref first_type) => {
+            Type::Array(ref first_type, _length) => {
                 if let Type::TypePath(_) = **first_type {
                     if last_part == "u8" {
                         return BasicType::FIXED_LEN_BYTE_ARRAY;
@@ -638,17 +636,38 @@ impl Type {
             }
             "f32" => BasicType::FLOAT,
             "f64" => BasicType::DOUBLE,
-            "String" | "str" | "Uuid" => BasicType::BYTE_ARRAY,
+            "String" | "str" => BasicType::BYTE_ARRAY,
+            "Uuid" => BasicType::FIXED_LEN_BYTE_ARRAY,
             f => unimplemented!("{} currently is not supported", f),
         }
     }
 
+    fn length(&self) -> Option<syn::Expr> {
+        let last_part = self.last_part();
+        let leaf_type = self.leaf_type_recursive();
+
+        // `[u8; N]` => Some(N)
+        if let Type::Array(ref first_type, length) = leaf_type {
+            if let Type::TypePath(_) = **first_type {
+                if last_part == "u8" {
+                    return Some(length.clone());
+                }
+            }
+        }
+
+        match last_part.trim() {
+            // Uuid => [u8; 16] => Some(16)
+            "Uuid" => Some(syn::parse_quote!(16)),
+            _ => None,
+        }
+    }
+
     fn logical_type(&self) -> proc_macro2::TokenStream {
         let last_part = self.last_part();
         let leaf_type = self.leaf_type_recursive();
 
         match leaf_type {
-            Type::Array(ref first_type) => {
+            Type::Array(ref first_type, _length) => {
                 if let Type::TypePath(_) = **first_type {
                     if last_part == "u8" {
                         return quote! { None };
@@ -789,7 +808,7 @@ impl Type {
 
     fn from_type_array(f: &syn::Field, ta: &syn::TypeArray) -> Self {
         let inner_type = Type::from_type(f, ta.elem.as_ref());
-        Type::Array(Box::new(inner_type))
+        Type::Array(Box::new(inner_type), ta.len.clone())
     }
 
     fn from_type_slice(f: &syn::Field, ts: &syn::TypeSlice) -> Self {
@@ -1091,6 +1110,7 @@ mod test {
             a_fix_byte_buf: [u8; 10],
             a_complex_option: ::std::option::Option<&Vec<u8>>,
             a_complex_vec: &::std::vec::Vec<&Option<u8>>,
+            a_uuid: ::uuid::Uuid,
           }
         };
 
@@ -1110,7 +1130,42 @@ mod test {
                 BasicType::BYTE_ARRAY,
                 BasicType::FIXED_LEN_BYTE_ARRAY,
                 BasicType::BYTE_ARRAY,
-                BasicType::INT32
+                BasicType::INT32,
+                BasicType::FIXED_LEN_BYTE_ARRAY,
+            ]
+        )
+    }
+
+    #[test]
+    fn test_type_length() {
+        let snippet: proc_macro2::TokenStream = quote! {
+          struct LotsOfInnerTypes {
+            a_buf: ::std::vec::Vec<u8>,
+            a_number: i32,
+            a_verbose_option: ::std::option::Option<bool>,
+            a_silly_string: String,
+            a_fix_byte_buf: [u8; 10],
+            a_complex_option: ::std::option::Option<&Vec<u8>>,
+            a_complex_vec: &::std::vec::Vec<&Option<u8>>,
+            a_uuid: ::uuid::Uuid,
+          }
+        };
+
+        let fields = extract_fields(snippet);
+        let converted_fields: Vec<_> = fields.iter().map(Type::from).collect();
+        let lengths: Vec<_> = converted_fields.iter().map(|ty| ty.length()).collect();
+
+        assert_eq!(
+            lengths,
+            vec![
+                None,
+                None,
+                None,
+                None,
+                Some(syn::parse_quote!(10)),
+                None,
+                None,
+                Some(syn::parse_quote!(16)),
             ]
         )
     }
@@ -1328,8 +1383,8 @@ mod test {
         let when = Field::from(&fields[0]);
         assert_eq!(when.writer_snippet().to_string(),(quote!{
             {
-                let vals : Vec<_> = records.iter().map(|rec| (&rec.unique_id.to_string()[..]).into() ).collect();
-                if let ColumnWriter::ByteArrayColumnWriter(ref mut typed) = column_writer.untyped() {
+                let vals : Vec<_> = records.iter().map(|rec| rec.unique_id.as_bytes().to_vec().into() ).collect();
+                if let ColumnWriter::FixedLenByteArrayColumnWriter(ref mut typed) = column_writer.untyped() {
                     typed.write_batch(&vals[..], None, None) ?;
                 } else {
                     panic!("Schema and struct disagree on type for {}" , stringify!{ unique_id })
@@ -1349,7 +1404,7 @@ mod test {
                     }
                 }).collect();
 
-                if let ColumnWriter::ByteArrayColumnWriter(ref mut typed) = column_writer.untyped() {
+                if let ColumnWriter::FixedLenByteArrayColumnWriter(ref mut typed) = column_writer.untyped() {
                     typed.write_batch(&vals[..], Some(&definition_levels[..]), None) ?;
                 } else {
                     panic!("Schema and struct disagree on type for {}" , stringify!{ maybe_unique_id })
@@ -1371,13 +1426,13 @@ mod test {
         assert_eq!(when.reader_snippet().to_string(),(quote!{
             {
                 let mut vals = Vec::new();
-                if let ColumnReader::ByteArrayColumnReader(mut typed) = column_reader {
+                if let ColumnReader::FixedLenByteArrayColumnReader(mut typed) = column_reader {
                     typed.read_records(num_records, None, None, &mut vals)?;
                 } else {
                     panic!("Schema and struct disagree on type for {}", stringify!{ unique_id });
                 }
                 for (i, r) in &mut records[..num_records].iter_mut().enumerate() {
-                    r.unique_id = ::uuid::Uuid::parse_str(vals[i].data().convert()).unwrap();
+                    r.unique_id = ::uuid::Uuid::from_bytes(vals[i].data().try_into().unwrap());
                 }
             }
         }).to_string());


Test patch:
diff --git a/parquet_derive_test/Cargo.toml b/parquet_derive_test/Cargo.toml
index a5d2e76d4..a3a4b58fe 100644
--- a/parquet_derive_test/Cargo.toml
+++ b/parquet_derive_test/Cargo.toml
@@ -32,3 +32,4 @@ rust-version = { workspace = true }
 parquet = { workspace = true }
 parquet_derive = { path = "../parquet_derive", default-features = false }
 chrono = { workspace = true }
+uuid = { version = "1", features = ["v4"] }
diff --git a/parquet_derive_test/src/lib.rs b/parquet_derive_test/src/lib.rs
index 25734813a..3743c6b55 100644
--- a/parquet_derive_test/src/lib.rs
+++ b/parquet_derive_test/src/lib.rs
@@ -42,6 +42,7 @@ struct ACompleteRecord<'a> {
     pub borrowed_maybe_a_string: &'a Option<String>,
     pub borrowed_maybe_a_str: &'a Option<&'a str>,
     pub now: chrono::NaiveDateTime,
+    pub uuid: uuid::Uuid,
     pub byte_vec: Vec<u8>,
     pub maybe_byte_vec: Option<Vec<u8>>,
     pub borrowed_byte_vec: &'a [u8],
@@ -61,6 +62,7 @@ struct APartiallyCompleteRecord {
     pub double: f64,
     pub now: chrono::NaiveDateTime,
     pub date: chrono::NaiveDate,
+    pub uuid: uuid::Uuid,
     pub byte_vec: Vec<u8>,
 }
 
@@ -105,6 +107,7 @@ mod tests {
             OPTIONAL BINARY          borrowed_maybe_a_string (STRING);
             OPTIONAL BINARY          borrowed_maybe_a_str (STRING);
             REQUIRED INT64           now (TIMESTAMP_MILLIS);
+            REQUIRED FIXED_LEN_BYTE_ARRAY (16) uuid (UUID);
             REQUIRED BINARY          byte_vec;
             OPTIONAL BINARY          maybe_byte_vec;
             REQUIRED BINARY          borrowed_byte_vec;
@@ -144,6 +147,7 @@ mod tests {
             borrowed_maybe_a_string: &maybe_a_string,
             borrowed_maybe_a_str: &maybe_a_str,
             now: chrono::Utc::now().naive_local(),
+            uuid: uuid::Uuid::new_v4(),
             byte_vec: vec![0x65, 0x66, 0x67],
             maybe_byte_vec: Some(vec![0x88, 0x89, 0x90]),
             borrowed_byte_vec: &borrowed_byte_vec,
@@ -179,6 +183,7 @@ mod tests {
             double: std::f64::NAN,
             now: chrono::Utc::now().naive_local(),
             date: chrono::naive::NaiveDate::from_ymd_opt(2015, 3, 14).unwrap(),
+            uuid: uuid::Uuid::new_v4(),
             byte_vec: vec![0x65, 0x66, 0x67],
         }];