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

alamb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new 8bac91d73 Panic instead of discarding nulls converting StructArray to 
RecordBatch -  (#3951) (#3953)
8bac91d73 is described below

commit 8bac91d732bc3cfc01ecb2869bce013c9a6cf2e5
Author: Raphael Taylor-Davies <[email protected]>
AuthorDate: Sat Apr 1 11:27:49 2023 +0100

    Panic instead of discarding nulls converting StructArray to RecordBatch -  
(#3951) (#3953)
    
    * Don't discard nulls converting StructArray to RecordBatch (#3951)
    
    * Add more docs
---
 arrow-array/src/array/struct_array.rs          | 32 ++++++++++++++++-
 arrow-array/src/record_batch.rs                | 48 +++++++++++++++++---------
 arrow/src/ffi_stream.rs                        |  4 +--
 parquet/src/arrow/array_reader/struct_array.rs |  5 +--
 parquet/src/arrow/arrow_reader/mod.rs          | 14 ++++----
 5 files changed, 75 insertions(+), 28 deletions(-)

diff --git a/arrow-array/src/array/struct_array.rs 
b/arrow-array/src/array/struct_array.rs
index 0604f71d3..e31594d4b 100644
--- a/arrow-array/src/array/struct_array.rs
+++ b/arrow-array/src/array/struct_array.rs
@@ -18,12 +18,34 @@
 use crate::{make_array, Array, ArrayRef, RecordBatch};
 use arrow_buffer::{buffer_bin_or, Buffer, NullBuffer};
 use arrow_data::ArrayData;
-use arrow_schema::{ArrowError, DataType, Field, SchemaBuilder};
+use arrow_schema::{ArrowError, DataType, Field, Fields, SchemaBuilder};
 use std::sync::Arc;
 use std::{any::Any, ops::Index};
 
 /// A nested array type where each child (called *field*) is represented by a 
separate
 /// array.
+///
+///
+/// # Comparison with [RecordBatch]
+///
+/// Both [`RecordBatch`] and [`StructArray`] represent a collection of columns 
/ arrays with the
+/// same length.
+///
+/// However, there are a couple of key differences:
+///
+/// * [`StructArray`] can be nested within other [`Array`], including itself
+/// * [`RecordBatch`] can contain top-level metadata on its associated 
[`Schema`][arrow_schema::Schema]
+/// * [`StructArray`] can contain top-level nulls, i.e. `null`
+/// * [`RecordBatch`] can only represent nulls in its child columns, i.e. 
`{"field": null}`
+///
+/// [`StructArray`] is therefore a more general data container than 
[`RecordBatch`], and as such
+/// code that needs to handle both will typically share an implementation in 
terms of
+/// [`StructArray`] and convert to/from [`RecordBatch`] as necessary.
+///
+/// [`From`] implementations are provided to facilitate this conversion, 
however, converting
+/// from a [`StructArray`] containing top-level nulls to a [`RecordBatch`] 
will panic, as there
+/// is no way to preserve them.
+///
 /// # Example: Create an array from a vector of fields
 ///
 /// ```
@@ -89,6 +111,14 @@ impl StructArray {
         }
     }
 
+    /// Returns the [`Fields`] of this [`StructArray`]
+    pub fn fields(&self) -> &Fields {
+        match self.data_type() {
+            DataType::Struct(f) => f,
+            _ => unreachable!(),
+        }
+    }
+
     /// Return child array whose field name equals to column_name
     ///
     /// Note: A schema can currently have duplicate field names, in which case
diff --git a/arrow-array/src/record_batch.rs b/arrow-array/src/record_batch.rs
index db4bb1230..081bd55fc 100644
--- a/arrow-array/src/record_batch.rs
+++ b/arrow-array/src/record_batch.rs
@@ -446,23 +446,28 @@ impl Default for RecordBatchOptions {
         Self::new()
     }
 }
+impl From<StructArray> for RecordBatch {
+    fn from(value: StructArray) -> Self {
+        assert_eq!(
+            value.null_count(),
+            0,
+            "Cannot convert nullable StructArray to RecordBatch, see 
StructArray documentation"
+        );
+        let row_count = value.len();
+        let schema = Arc::new(Schema::new(value.fields().clone()));
+        let columns = value.boxed_fields;
+
+        RecordBatch {
+            schema,
+            row_count,
+            columns,
+        }
+    }
+}
+
 impl From<&StructArray> for RecordBatch {
-    /// Create a record batch from struct array, where each field of
-    /// the `StructArray` becomes a `Field` in the schema.
-    ///
-    /// This currently does not flatten and nested struct types
     fn from(struct_array: &StructArray) -> Self {
-        if let DataType::Struct(fields) = struct_array.data_type() {
-            let schema = Schema::new(fields.clone());
-            let columns = struct_array.boxed_fields.clone();
-            RecordBatch {
-                schema: Arc::new(schema),
-                row_count: struct_array.len(),
-                columns,
-            }
-        } else {
-            unreachable!("unable to get datatype as struct")
-        }
+        struct_array.clone().into()
     }
 }
 
@@ -558,7 +563,7 @@ mod tests {
         BooleanArray, Int32Array, Int64Array, Int8Array, ListArray, 
StringArray,
     };
     use arrow_buffer::{Buffer, ToByteSlice};
-    use arrow_data::ArrayDataBuilder;
+    use arrow_data::{ArrayData, ArrayDataBuilder};
     use arrow_schema::Fields;
 
     #[test]
@@ -1046,4 +1051,15 @@ mod tests {
         assert!(!options.match_field_names);
         assert_eq!(options.row_count.unwrap(), 20)
     }
+
+    #[test]
+    #[should_panic(expected = "Cannot convert nullable StructArray to 
RecordBatch")]
+    fn test_from_struct() {
+        let s = StructArray::from(ArrayData::new_null(
+            // Note child is not nullable
+            &DataType::Struct(vec![Field::new("foo", DataType::Int32, 
false)].into()),
+            2,
+        ));
+        let _ = RecordBatch::from(s);
+    }
 }
diff --git a/arrow/src/ffi_stream.rs b/arrow/src/ffi_stream.rs
index b1046d142..6b3067ab7 100644
--- a/arrow/src/ffi_stream.rs
+++ b/arrow/src/ffi_stream.rs
@@ -373,7 +373,7 @@ impl Iterator for ArrowArrayStreamReader {
             .to_data()
             .ok()?;
 
-            let record_batch = RecordBatch::from(&StructArray::from(data));
+            let record_batch = RecordBatch::from(StructArray::from(data));
 
             Some(Ok(record_batch))
         } else {
@@ -492,7 +492,7 @@ mod tests {
             .to_data()
             .unwrap();
 
-            let record_batch = RecordBatch::from(&StructArray::from(array));
+            let record_batch = RecordBatch::from(StructArray::from(array));
             produced_batches.push(record_batch);
         }
 
diff --git a/parquet/src/arrow/array_reader/struct_array.rs 
b/parquet/src/arrow/array_reader/struct_array.rs
index 22724ae3f..0670701a0 100644
--- a/parquet/src/arrow/array_reader/struct_array.rs
+++ b/parquet/src/arrow/array_reader/struct_array.rs
@@ -217,6 +217,7 @@ mod tests {
     use crate::arrow::array_reader::ListArrayReader;
     use arrow::buffer::Buffer;
     use arrow::datatypes::Field;
+    use arrow_array::cast::AsArray;
     use arrow_array::{Array, Int32Array, ListArray};
     use arrow_schema::Fields;
 
@@ -252,7 +253,7 @@ mod tests {
         );
 
         let struct_array = struct_array_reader.next_batch(5).unwrap();
-        let struct_array = 
struct_array.as_any().downcast_ref::<StructArray>().unwrap();
+        let struct_array = struct_array.as_struct();
 
         assert_eq!(5, struct_array.len());
         assert_eq!(
@@ -328,7 +329,7 @@ mod tests {
         );
 
         let actual = struct_reader.next_batch(1024).unwrap();
-        let actual = actual.as_any().downcast_ref::<StructArray>().unwrap();
+        let actual = actual.as_struct();
         assert_eq!(actual, &expected)
     }
 }
diff --git a/parquet/src/arrow/arrow_reader/mod.rs 
b/parquet/src/arrow/arrow_reader/mod.rs
index ba322e29d..4b88a33f3 100644
--- a/parquet/src/arrow/arrow_reader/mod.rs
+++ b/parquet/src/arrow/arrow_reader/mod.rs
@@ -20,7 +20,8 @@
 use std::collections::VecDeque;
 use std::sync::Arc;
 
-use arrow_array::{Array, StructArray};
+use arrow_array::cast::AsArray;
+use arrow_array::Array;
 use arrow_array::{RecordBatch, RecordBatchReader};
 use arrow_schema::{ArrowError, DataType as ArrowType, Schema, SchemaRef};
 use arrow_select::filter::prep_null_mask_filter;
@@ -559,12 +560,11 @@ impl Iterator for ParquetRecordBatchReader {
         match self.array_reader.consume_batch() {
             Err(error) => Some(Err(error.into())),
             Ok(array) => {
-                let struct_array =
-                    array.as_any().downcast_ref::<StructArray>().ok_or_else(|| 
{
-                        ArrowError::ParquetError(
-                            "Struct array reader should return struct 
array".to_string(),
-                        )
-                    });
+                let struct_array = array.as_struct_opt().ok_or_else(|| {
+                    ArrowError::ParquetError(
+                        "Struct array reader should return struct 
array".to_string(),
+                    )
+                });
 
                 match struct_array {
                     Err(err) => Some(Err(err)),

Reply via email to