From 7dad4e682e2fe4d4bc8d007118db6b78158fe764 Mon Sep 17 00:00:00 2001 From: Peter Bower <37089506+pbower@users.noreply.github.com> Date: Wed, 16 Sep 2026 03:54:44 +0100 Subject: [PATCH] Add resolve_batches, confirm_batch_dtype and entry-type From impls --- src/enums/array.rs | 97 ++++ src/structs/chunked/super_array.rs | 92 +++- src/structs/chunked/super_table.rs | 10 + src/structs/views/chunked/super_array_view.rs | 415 +++++++++++++++++- src/structs/views/chunked/super_table_view.rs | 9 + 5 files changed, 614 insertions(+), 9 deletions(-) diff --git a/src/enums/array.rs b/src/enums/array.rs index e5bf87d..dc1c46f 100644 --- a/src/enums/array.rs +++ b/src/enums/array.rs @@ -2810,6 +2810,103 @@ impl Array { } } + /// Checks whether this array's dtype matches the target Field, and + /// converts it when possible using the existing cast methods. + /// + /// The function exists to support the `allow_mixed_array_batches` + /// mode which relaxes uniform `SuperArray` typing restrictions. + /// When this method is ran, it coerces to the target `Field` type. + /// + /// ## Conversion rules + /// + /// | Input | Result | + /// |---|---| + /// | Matching dtype | Original array, without copying | + /// | Different numeric width | Corresponding `NumericArray::try_i32`, `try_i64`, `try_u32`, `try_u64`, `try_f32`, or `try_f64` conversion | + /// | Datetime32 to Datetime64 | `TemporalArray::try_dt64` | + /// | Datetime64 to Datetime32 | `TemporalArray::try_dt32` | + /// | String32 to String64 (`LargeString`) | `From<&StringArray> for StringArray` | + /// | Unsupported conversion | All-null array with the target dtype and source length, created through `Array::null_array` | + /// | Empty input | Zero-row array with the target field dtype | + #[cfg(feature = "allow_mixed_array_batches")] + pub fn check_unify_batch_dtype(array: Array, field: &crate::Field) -> Array { + use crate::ffi::arrow_dtype::ArrowType; + + let len = array.len(); + + // Empty array: zero-row array of the Field dtype. + if len == 0 { + return Array::from_arrow_dtype(&field.dtype); + } + + // Dtype already matches: pass through with no copy. + if array.arrow_type() == field.dtype { + return array; + } + + let converted = match (&array, &field.dtype) { + // Numeric array at another numeric width. + (Array::NumericArray(num), ArrowType::Int32) => { + num.try_i32().ok().map(|a| Array::NumericArray(NumericArray::Int32(a))) + } + (Array::NumericArray(num), ArrowType::Int64) => { + num.try_i64().ok().map(|a| Array::NumericArray(NumericArray::Int64(a))) + } + (Array::NumericArray(num), ArrowType::UInt32) => { + num.try_u32().ok().map(|a| Array::NumericArray(NumericArray::UInt32(a))) + } + (Array::NumericArray(num), ArrowType::UInt64) => { + num.try_u64().ok().map(|a| Array::NumericArray(NumericArray::UInt64(a))) + } + (Array::NumericArray(num), ArrowType::Float32) => { + num.try_f32().ok().map(|a| Array::NumericArray(NumericArray::Float32(a))) + } + (Array::NumericArray(num), ArrowType::Float64) => { + num.try_f64().ok().map(|a| Array::NumericArray(NumericArray::Float64(a))) + } + + // Datetime32 under a Datetime64 Field. + #[cfg(feature = "datetime")] + (Array::TemporalArray(temp), target) + if matches!( + target, + ArrowType::Date64 + | ArrowType::Time64(_) + | ArrowType::Duration64(_) + | ArrowType::Timestamp(_, _) + ) => + { + temp.try_dt64() + .ok() + .map(|a| Array::TemporalArray(crate::TemporalArray::Datetime64(a))) + } + + // Datetime64 under a Datetime32 Field. + #[cfg(feature = "datetime")] + (Array::TemporalArray(temp), target) + if matches!( + target, + ArrowType::Date32 | ArrowType::Time32(_) | ArrowType::Duration32(_) + ) => + { + temp.try_dt32() + .ok() + .map(|a| Array::TemporalArray(crate::TemporalArray::Datetime32(a))) + } + + // String32 under a String64 (LargeString) Field. + #[cfg(feature = "large_string")] + (Array::TextArray(crate::TextArray::String32(s32)), ArrowType::LargeString) => { + let widened = crate::StringArray::::from(&**s32); + Some(Array::TextArray(crate::TextArray::String64(Arc::new(widened)))) + } + + _ => None, + }; + + converted.unwrap_or_else(|| Array::null_array(&field.dtype, len)) + } + /// Build an array from a slice of Scalars. /// /// All scalars must be the same type. The type is inferred from the first diff --git a/src/structs/chunked/super_array.rs b/src/structs/chunked/super_array.rs index f48615e..cd10233 100644 --- a/src/structs/chunked/super_array.rs +++ b/src/structs/chunked/super_array.rs @@ -45,11 +45,11 @@ //! Field-based constructors and `push_field_array` also provide `try_*` //! variants that return `Result` when runtime data may not match the required //! type. -//! +//! //! Mixed cases cannot be used in `SuperTable` and are rejected at the boundary //! as it would violate contractual `Field`-based guarantees. Hence, these are -//! intended for transient workloads only. -//! +//! intended for transient workloads only. +//! //! ## Apache Arrow / Polars bridges (`cast_arrow` / `cast_polars` features) //! - `to_apache_arrow()` exports each chunk as an arrow-rs `ArrayRef`. //! - `to_polars()` builds a polars `Series` whose internal chunks mirror the SuperArray. @@ -258,7 +258,7 @@ impl SuperArray { /// Constructs a SuperArray from raw `Array` chunks with null counts. /// /// # Panics - /// 1. If null_counts length does not match chunks length. + /// 1. If null_counts length does not match chunks length. /// 2. On mismatched chunk types, unless the `allow_mixed_array_batches`feature is on. pub fn from_arrays_nc(chunks: Vec, null_counts: Vec) -> Self { assert_eq!( @@ -1139,6 +1139,70 @@ impl SuperArray { chunks.all(|chunk| chunk.arrow_type() == dtype) } + /// Resolves every chunk to the container's target dtype. + /// + /// ## Behaviour + /// - The target dtype is the Field when present, otherwise the type of + /// the first batch. + /// - The result ends up with a Field that describes every chunk. + /// - For a field-free SuperArray, a Field is constructed from the first chunk's type. + /// - Chunks that already match the target pass through without penalty. + /// - The conversion per chunk delegates to `Array::check_unify_batch_dtype`. + /// - Categorical dictionaries are merged across the resolved chunks + /// via `rebuild_category_manager` when the `shared_dict` feature is on. + #[cfg(all(feature = "allow_mixed_array_batches", feature = "views"))] + pub fn resolve_batches(self) -> SuperArray { + if self.chunks.is_empty() { + return self; + } + + // Target dtype is the Field when present, otherwise the first chunk. + let field = match self.field.clone() { + Some(f) => f, + None => { + let first = &self.chunks[0]; + Arc::new(Field::new( + "data", + first.arrow_type(), + first.is_nullable(), + None, + )) + } + }; + + // Fast path - all chunks already match the target. + if self + .chunks + .iter() + .all(|chunk| chunk.arrow_type() == field.dtype) + { + let mut sa = self; + if sa.field.is_none() { + sa.field = Some(field); + } + return sa; + } + + // Convert each chunk through check_unify_batch_dtype. + let chunks: Vec = self + .chunks + .into_iter() + .map(|chunk| Array::check_unify_batch_dtype(chunk, &field)) + .collect(); + + #[cfg_attr(not(feature = "shared_dict"), allow(unused_mut))] + let mut sa = SuperArray { + chunks, + field: Some(field), + null_counts: None, + #[cfg(feature = "shared_dict")] + category_manager: None, + }; + #[cfg(feature = "shared_dict")] + sa.rebuild_category_manager(); + sa + } + /// Borrow the column's `CategoryManagerT`, or `None` if the column /// is not categorical or no chunks have been pushed yet. /// @@ -1250,6 +1314,19 @@ impl FromIterator for SuperArray { } } +/// Consolidates all chunks into a single contiguous `ArrayV`. +/// +/// Empty SuperArrays produce a zero-row array of the Field dtype +/// when a Field is present, or `Array::Null` otherwise. +#[cfg(feature = "views")] +impl From for ArrayV { + fn from(sa: SuperArray) -> Self { + #[cfg(feature = "allow_mixed_array_batches")] + let sa = sa.resolve_batches(); + ArrayV::from(sa.consolidate()) + } +} + impl Shape for SuperArray { fn shape(&self) -> ShapeDim { ShapeDim::Rank1(self.len()) @@ -1731,8 +1808,9 @@ mod tests { assert_eq!(sa.len(), 5); } - /// A `Field`-carrying constructor rejects mixed chunks with the feature - /// on: a present field always describes every chunk. + /// - `Field` holding constructor rejects mixed chunks with the feature + /// on. + /// - Presents field instance describes every chunk. #[cfg(feature = "allow_mixed_array_batches")] #[test] #[should_panic(expected = "ArrowType mismatch")] @@ -1746,7 +1824,7 @@ mod tests { ); } - /// A push onto a `Field`-carrying SuperArray rejects a mismatched chunk + /// A push onto a `Field`-holding SuperArray rejects a mismatched chunk /// with the feature on. #[cfg(feature = "allow_mixed_array_batches")] #[test] diff --git a/src/structs/chunked/super_table.rs b/src/structs/chunked/super_table.rs index 3f2df9e..cada877 100644 --- a/src/structs/chunked/super_table.rs +++ b/src/structs/chunked/super_table.rs @@ -1199,6 +1199,16 @@ impl From for SuperTable { } } +/// Consolidates all batches into a single `TableV`. +/// +/// Empty SuperTables produce an empty `TableV` via `Table::default`. +#[cfg(feature = "views")] +impl From for TableV { + fn from(st: SuperTable) -> Self { + TableV::from(st.consolidate()) + } +} + /// Ergonomic constructor for a [`SuperTable`] from named table batches. /// /// Each batch argument may be a `Table` or `Arc`; both flow diff --git a/src/structs/views/chunked/super_array_view.rs b/src/structs/views/chunked/super_array_view.rs index 0b05640..0ff5199 100644 --- a/src/structs/views/chunked/super_array_view.rs +++ b/src/structs/views/chunked/super_array_view.rs @@ -49,11 +49,14 @@ use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use crate::{ - Array, ArrayV, ArrayVT, Field, SuperArray, + Array, ArrayV, ArrayVT, BooleanArray, BooleanArrayV, Field, NumericArray, NumericArrayV, + StringArray, SuperArray, TextArray, TextArrayV, enums::error::MinarrowError, enums::shape_dim::ShapeDim, traits::{concatenate::Concatenate, consolidate::Consolidate, shape::Shape}, }; +#[cfg(feature = "datetime")] +use crate::{TemporalArray, TemporalArrayV}; /// # SuperArrayView /// @@ -112,6 +115,56 @@ impl SuperArrayV { .all(|slice| slice.array.arrow_type() == self.field.dtype) } + /// Converts each batch to the dtype declared by this view's `Field` using + /// `Array::check_unify_batch_dtype`. + /// + /// ## Behaviour + /// + /// - Uses the view's field dtype as the conversion target. + /// - Reuses matching batches without copying. + /// - Returns a `SuperArrayV` with a consistent dtype across all batches. + /// - Merges categorical dictionaries after converting each batch. + #[cfg(feature = "allow_mixed_array_batches")] + pub fn resolve_batches(self) -> SuperArrayV { + if self.slices.is_empty() { + return self; + } + + // Fast path: all slices already match the Field dtype. + if self.check_type_uniformity() { + return self; + } + + let field = self.field.clone(); + let mut arrays: Vec = self + .slices + .into_iter() + .map(|slice| Array::check_unify_batch_dtype(slice.to_array(), &field)) + .collect(); + + let len = arrays.iter().map(|a| a.len()).sum(); + + // Categorical columns: merge dictionaries across all resolved + // batches so codes are mutually meaningful. + #[cfg(feature = "shared_dict")] + { + use crate::ffi::arrow_dtype::ArrowType; + if matches!(field.dtype, ArrowType::Dictionary(_)) { + let mut mgr: Option = None; + crate::structs::dictionary::CategoryManagerT::add_remap_cats( + &mut mgr, + arrays.iter_mut(), + ); + } + } + + SuperArrayV { + slices: arrays.into_iter().map(ArrayV::from).collect(), + len, + field, + } + } + /// Returns a sub-window of this chunked array view over `[offset .. offset+len)`. /// /// Produces a new `ChunkedArrayView` with updated slice metadata. @@ -225,7 +278,7 @@ impl Consolidate for SuperArrayV { /// 3. Otherwise: directly extends from source data slices (single copy per element). fn consolidate(self) -> Array { if self.slices.is_empty() { - panic!("consolidate() called on empty SuperArrayV"); + return Array::from_arrow_dtype(&self.field.dtype); } // Single slice optimisation @@ -317,6 +370,72 @@ impl From for SuperArray { } } +/// Consolidates all slices into a single contiguous `ArrayV`. +impl From for ArrayV { + fn from(view: SuperArrayV) -> Self { + #[cfg(feature = "allow_mixed_array_batches")] + let view = view.resolve_batches(); + ArrayV::from(view.consolidate()) + } +} + +/// Consolidates and extracts the numeric family. Non-numeric arrays +/// produce `NumericArray::Null`. +impl From for NumericArrayV { + fn from(view: SuperArrayV) -> Self { + let arr_v: ArrayV = view.into(); + match arr_v.array { + Array::NumericArray(_) => NumericArrayV::from(arr_v), + _ => NumericArrayV::from(ArrayV::from( + Array::NumericArray(NumericArray::Null), + )), + } + } +} + +/// Consolidates and extracts the text family. Non-text arrays produce +/// a zero-row `String32`. +impl From for TextArrayV { + fn from(view: SuperArrayV) -> Self { + let arr_v: ArrayV = view.into(); + match arr_v.array { + Array::TextArray(_) => TextArrayV::from(arr_v), + _ => TextArrayV::from(ArrayV::from(Array::TextArray( + TextArray::String32(Arc::new(StringArray::::default())), + ))), + } + } +} + +/// Consolidates and extracts the temporal family. Non-temporal arrays +/// produce `TemporalArray::Null`. +#[cfg(feature = "datetime")] +impl From for TemporalArrayV { + fn from(view: SuperArrayV) -> Self { + let arr_v: ArrayV = view.into(); + match arr_v.array { + Array::TemporalArray(_) => TemporalArrayV::from(arr_v), + _ => TemporalArrayV::from(ArrayV::from(Array::TemporalArray( + TemporalArray::Null, + ))), + } + } +} + +/// Consolidates and extracts the boolean family. Non-boolean arrays +/// produce a zero-row `BooleanArray`. +impl From for BooleanArrayV { + fn from(view: SuperArrayV) -> Self { + let arr_v: ArrayV = view.into(); + match arr_v.array { + Array::BooleanArray(_) => BooleanArrayV::from(arr_v), + _ => BooleanArrayV::from(ArrayV::from(Array::BooleanArray(Arc::new( + BooleanArray::default(), + )))), + } + } +} + impl Display for SuperArrayV { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { writeln!( @@ -1238,6 +1357,298 @@ mod tests { panic!("Expected Decimal32 Array"); } } + + // --------------------------------------------------------------- + // Empty-view consolidation fix + // --------------------------------------------------------------- + + #[test] + fn test_empty_view_consolidates_to_zero_row_array() { + let f = Arc::new(Field::new("col", ArrowType::Int32, false, None)); + let empty = SuperArrayV { + slices: Vec::new(), + len: 0, + field: f, + }; + let result = empty.consolidate(); + assert_eq!(result.len(), 0); + assert_eq!(result.arrow_type(), ArrowType::Int32); + } + + #[test] + fn test_empty_view_consolidates_to_zero_row_float64() { + let f = Arc::new(Field::new("x", ArrowType::Float64, false, None)); + let empty = SuperArrayV { + slices: Vec::new(), + len: 0, + field: f, + }; + let result = empty.consolidate(); + assert_eq!(result.len(), 0); + assert_eq!(result.arrow_type(), ArrowType::Float64); + } + + // --------------------------------------------------------------- + // check_unify_batch_dtype tests + // --------------------------------------------------------------- + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_check_unify_batch_dtype_pass_through_on_matching_type() { + let arr = Array::from_int32(crate::IntegerArray::::from_slice(&[1, 2, 3])); + let field = Field::new("x", ArrowType::Int32, false, None); + + let result = Array::check_unify_batch_dtype(arr, &field); + assert_eq!(result.len(), 3); + assert_eq!(result.arrow_type(), ArrowType::Int32); + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_check_unify_batch_dtype_numeric_i32_to_f64() { + let arr = Array::from_int32(crate::IntegerArray::::from_slice(&[1, 2, 3])); + let field = Field::new("x", ArrowType::Float64, false, None); + + let result = Array::check_unify_batch_dtype(arr, &field); + assert_eq!(result.len(), 3); + assert_eq!(result.arrow_type(), ArrowType::Float64); + if let Array::NumericArray(NumericArray::Float64(f)) = &result { + assert_eq!(f.data.as_slice(), &[1.0, 2.0, 3.0]); + } else { + panic!("expected Float64"); + } + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_check_unify_batch_dtype_numeric_f64_to_i64() { + let arr = Array::from_float64(crate::FloatArray::::from_slice(&[10.0, 20.0])); + let field = Field::new("x", ArrowType::Int64, false, None); + + let result = Array::check_unify_batch_dtype(arr, &field); + assert_eq!(result.len(), 2); + assert_eq!(result.arrow_type(), ArrowType::Int64); + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_check_unify_batch_dtype_incompatible_becomes_null() { + use crate::MaskedArray; + + let arr = Array::from_string32(crate::StringArray::::from_slice(&["a", "b"])); + let field = Field::new("x", ArrowType::Int32, false, None); + + let result = Array::check_unify_batch_dtype(arr, &field); + assert_eq!(result.len(), 2); + assert_eq!(result.arrow_type(), ArrowType::Int32); + + // Every element should be null. + if let Array::NumericArray(NumericArray::Int32(ints)) = &result { + assert_eq!(ints.null_count(), 2); + } else { + panic!("expected Int32"); + } + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_check_unify_batch_dtype_empty_batch() { + let arr = Array::from_int32(crate::IntegerArray::::from_slice(&[])); + let field = Field::new("x", ArrowType::Float64, false, None); + + let result = Array::check_unify_batch_dtype(arr, &field); + assert_eq!(result.len(), 0); + assert_eq!(result.arrow_type(), ArrowType::Float64); + } + + #[cfg(all(feature = "allow_mixed_array_batches", feature = "large_string"))] + #[test] + fn test_check_unify_batch_dtype_string32_to_string64() { + let arr = Array::from_string32(crate::StringArray::::from_slice(&["hello", "world"])); + let field = Field::new("x", ArrowType::LargeString, false, None); + + let result = Array::check_unify_batch_dtype(arr, &field); + assert_eq!(result.len(), 2); + assert_eq!(result.arrow_type(), ArrowType::LargeString); + } + + // --------------------------------------------------------------- + // resolve_batches tests + // --------------------------------------------------------------- + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_resolve_batches_uniform_pass_through() { + let field = Arc::new(Field::new("x", ArrowType::Int32, false, None)); + let view = SuperArrayV { + slices: vec![ + ArrayV::from(Array::from_int32( + crate::IntegerArray::::from_slice(&[1, 2]), + )), + ArrayV::from(Array::from_int32( + crate::IntegerArray::::from_slice(&[3]), + )), + ], + len: 3, + field, + }; + + let resolved = view.resolve_batches(); + assert_eq!(resolved.len, 3); + assert_eq!(resolved.slices.len(), 2); + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_resolve_batches_numeric_straggler() { + use crate::traits::consolidate::Consolidate; + + let field = Arc::new(Field::new("x", ArrowType::Float64, false, None)); + let view = SuperArrayV { + slices: vec![ + ArrayV::from(Array::from_float64( + crate::FloatArray::::from_slice(&[1.0, 2.0]), + )), + ArrayV::from(Array::from_int32( + crate::IntegerArray::::from_slice(&[3, 4]), + )), + ArrayV::from(Array::from_float64( + crate::FloatArray::::from_slice(&[5.0]), + )), + ], + len: 5, + field, + }; + + let resolved = view.resolve_batches(); + assert_eq!(resolved.len, 5); + assert!(resolved.check_type_uniformity()); + + // Consolidate and verify values. + let arr = resolved.consolidate(); + assert_eq!(arr.len(), 5); + if let Array::NumericArray(NumericArray::Float64(f)) = arr { + assert_eq!(f.data.as_slice(), &[1.0, 2.0, 3.0, 4.0, 5.0]); + } else { + panic!("expected Float64"); + } + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_resolve_batches_text_straggler_becomes_null() { + use crate::MaskedArray; + + let field = Arc::new(Field::new("x", ArrowType::Float64, false, None)); + let view = SuperArrayV { + slices: vec![ + ArrayV::from(Array::from_float64( + crate::FloatArray::::from_slice(&[1.0, 2.0]), + )), + ArrayV::from(Array::from_string32( + crate::StringArray::::from_slice(&["a"]), + )), + ], + len: 3, + field, + }; + + let resolved = view.resolve_batches(); + assert_eq!(resolved.len, 3); + assert!(resolved.check_type_uniformity()); + + // The text batch should be all-null. + let null_slice = &resolved.slices[1]; + assert_eq!(null_slice.len(), 1); + if let Array::NumericArray(NumericArray::Float64(f)) = &null_slice.array { + assert_eq!(f.null_count(), 1); + } else { + panic!("expected Float64 null batch"); + } + } + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_resolve_batches_empty_view() { + let field = Arc::new(Field::new("x", ArrowType::Int32, false, None)); + let empty = SuperArrayV { + slices: Vec::new(), + len: 0, + field, + }; + let resolved = empty.resolve_batches(); + assert!(resolved.slices.is_empty()); + assert_eq!(resolved.len, 0); + } + + // --------------------------------------------------------------- + // From for ArrayV + // --------------------------------------------------------------- + + #[cfg(feature = "allow_mixed_array_batches")] + #[test] + fn test_from_super_array_view_for_array_view() { + let field = Arc::new(Field::new("x", ArrowType::Float64, false, None)); + let view = SuperArrayV { + slices: vec![ + ArrayV::from(Array::from_float64( + crate::FloatArray::::from_slice(&[1.0, 2.0]), + )), + ArrayV::from(Array::from_int32( + crate::IntegerArray::::from_slice(&[3]), + )), + ], + len: 3, + field, + }; + + let arr_v: ArrayV = view.into(); + assert_eq!(arr_v.len(), 3); + assert_eq!(arr_v.array.arrow_type(), ArrowType::Float64); + } + + #[test] + fn test_from_super_array_view_for_numeric_array_view() { + let field = Arc::new(Field::new("x", ArrowType::Float64, false, None)); + let view = SuperArrayV { + slices: vec![ArrayV::from(Array::from_float64( + crate::FloatArray::::from_slice(&[1.0, 2.0]), + ))], + len: 2, + field, + }; + + let num_v: crate::NumericArrayV = view.into(); + assert_eq!(num_v.len(), 2); + } + + #[test] + fn test_from_super_array_view_for_boolean_array_view() { + let field = Arc::new(Field::new("x", ArrowType::Boolean, false, None)); + let view = SuperArrayV { + slices: vec![ArrayV::from(Array::BooleanArray(Arc::new( + crate::BooleanArray::from_slice(&[true, false]), + )))], + len: 2, + field, + }; + + let bool_v: crate::BooleanArrayV = view.into(); + assert_eq!(bool_v.len(), 2); + } + + #[test] + fn test_from_super_array_view_empty_to_array_view() { + let field = Arc::new(Field::new("x", ArrowType::Int32, false, None)); + let empty = SuperArrayV { + slices: Vec::new(), + len: 0, + field, + }; + let arr_v: ArrayV = empty.into(); + assert_eq!(arr_v.len(), 0); + assert_eq!(arr_v.array.arrow_type(), ArrowType::Int32); + } } /// SuperArray -> SuperArrayV conversion diff --git a/src/structs/views/chunked/super_table_view.rs b/src/structs/views/chunked/super_table_view.rs index 0ba992c..6cf872d 100644 --- a/src/structs/views/chunked/super_table_view.rs +++ b/src/structs/views/chunked/super_table_view.rs @@ -255,6 +255,15 @@ impl From for SuperTableV { } } +/// Consolidates all view slices into a single `TableV`. +/// +/// Empty views produce an empty `TableV` via `Table::default`. +impl From for TableV { + fn from(view: SuperTableV) -> Self { + TableV::from(view.consolidate()) + } +} + impl Display for SuperTableV { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { writeln!(