From e6f6af6300048710a0f3ab5a12bd0b09c4b2a0fc Mon Sep 17 00:00:00 2001 From: Matt Katz Date: Mon, 27 Jul 2026 11:10:04 +0100 Subject: [PATCH] reveal Arrow exporter fast paths via execute_until Signed-off-by: Matt Katz --- vortex-arrow/src/executor/byte.rs | 54 +++++++++- vortex-arrow/src/executor/dictionary.rs | 38 +++++++ vortex-arrow/src/executor/list.rs | 131 +++++++++++++++++++++--- vortex-arrow/src/executor/run_end.rs | 86 ++++++++++++---- vortex-arrow/src/executor/struct_.rs | 62 +++++++++++ 5 files changed, 334 insertions(+), 37 deletions(-) diff --git a/vortex-arrow/src/executor/byte.rs b/vortex-arrow/src/executor/byte.rs index d7a435cca91..7df3aae5701 100644 --- a/vortex-arrow/src/executor/byte.rs +++ b/vortex-arrow/src/executor/byte.rs @@ -13,6 +13,8 @@ use vortex_array::ArrayRef; use vortex_array::ArrayView; use vortex_array::Canonical; use vortex_array::ExecutionCtx; +use vortex_array::arrays::Chunked; +use vortex_array::arrays::Constant; use vortex_array::arrays::VarBin; use vortex_array::arrays::VarBinViewArray; use vortex_array::arrays::varbin::VarBinArraySlotsExt; @@ -22,6 +24,7 @@ use vortex_array::dtype::DType; use vortex_array::dtype::NativePType; use vortex_array::dtype::Nullability; use vortex_array::dtype::PType; +use vortex_array::matcher::Matcher; use vortex_error::VortexError; use vortex_error::VortexResult; use vortex_error::vortex_bail; @@ -29,6 +32,21 @@ use vortex_error::vortex_bail; use crate::byte_view::execute_varbinview_to_arrow; use crate::executor::validity::to_arrow_null_buffer; +/// Matches the encodings [`to_arrow_byte_array`] requires for export. +/// +/// `Chunked` and `Constant` are matched to stop execution before it destroys them: they have +/// specialized `append_to_builder` impls (chunk-wise append, scalar repeat) that the builder +/// fallback exploits. +struct ArrowByteExportable; + +impl Matcher for ArrowByteExportable { + type Match<'a> = &'a ArrayRef; + + fn try_match(array: &ArrayRef) -> Option> { + (array.is::() || array.is::() || array.is::()).then_some(array) + } +} + /// Convert a Vortex array into an Arrow GenericBinaryArray. pub(super) fn to_arrow_byte_array( array: ArrayRef, @@ -57,7 +75,9 @@ where return arrow_cast::cast(&binary_view, &T::DATA_TYPE).map_err(VortexError::from); } - // If the Vortex array is already in VarBin format, we can directly convert it. + let array = array.execute_until::(ctx)?; + + // If the Vortex array is in VarBin format, we can directly convert it. if let Some(array) = array.as_opt::() { return varbin_to_byte_array::(array, ctx); } @@ -106,15 +126,47 @@ mod tests { use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::array_session; + use vortex_array::arrays::BoolArray; use vortex_array::arrays::PrimitiveArray; + use vortex_array::arrays::VarBinArray; use vortex_array::arrays::VarBinViewArray; + use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt; use vortex_array::dtype::DType; use vortex_array::dtype::Nullability; + use vortex_array::scalar_fn::EmptyOptions; + use vortex_array::scalar_fn::fns::mask::Mask as MaskFn; use vortex_error::VortexResult; use vortex_mask::Mask; use crate::ArrowSessionExt; + #[test] + fn mask_wrapped_varbin_exports() -> VortexResult<()> { + let session = array_session(); + let mut ctx = session.create_execution_ctx(); + + let varbin = VarBinArray::from_vec( + vec!["hello", "world", "vortex"], + DType::Utf8(Nullability::NonNullable), + ); + let mask = BoolArray::from_iter([true, false, true]); + let masked = + MaskFn.try_new_array(3, EmptyOptions, [varbin.into_array(), mask.into_array()])?; + + let field = Field::new("s", DataType::Utf8, true); + let arrow = session + .arrow() + .execute_arrow(masked, Some(&field), &mut ctx)?; + + let strings = arrow.as_string::(); + assert_eq!(strings.len(), 3); + assert!(!strings.is_null(0)); + assert!(strings.is_null(1)); + assert_eq!(strings.value(0), "hello"); + assert_eq!(strings.value(2), "vortex"); + Ok(()) + } + fn make_utf8_array() -> VarBinViewArray { VarBinViewArray::from_iter_str(["hello", "world", "this is a longer string for testing"]) } diff --git a/vortex-arrow/src/executor/dictionary.rs b/vortex-arrow/src/executor/dictionary.rs index f5be1076a4b..ca730fc1ab2 100644 --- a/vortex-arrow/src/executor/dictionary.rs +++ b/vortex-arrow/src/executor/dictionary.rs @@ -18,18 +18,32 @@ use vortex_array::arrays::ConstantArray; use vortex_array::arrays::Dict; use vortex_array::arrays::DictArray; use vortex_array::arrays::dict::DictArraySlotsExt; +use vortex_array::matcher::Matcher; use vortex_error::VortexError; use vortex_error::VortexResult; use vortex_error::vortex_bail; use crate::ArrowArrayExecutor; +/// Matches the encodings [`to_arrow_dictionary`] requires for export. +struct ArrowDictExportable; + +impl Matcher for ArrowDictExportable { + type Match<'a> = &'a ArrayRef; + + fn try_match(array: &ArrayRef) -> Option> { + (array.is::() || array.is::()).then_some(array) + } +} + pub(super) fn to_arrow_dictionary( array: ArrayRef, codes_type: &DataType, values_type: &DataType, ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = array.execute_until::(ctx)?; + let array = match array.try_downcast::() { Ok(dict) => return dict_to_dict(dict, codes_type, values_type, ctx), Err(array) => array, @@ -141,6 +155,7 @@ mod tests { use std::sync::Arc; use arrow_array::DictionaryArray as ArrowDictArray; + use arrow_array::StringArray; use arrow_array::types::UInt8Type; use arrow_array::types::UInt32Type; use arrow_schema::DataType; @@ -148,11 +163,15 @@ mod tests { use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::array_session; + use vortex_array::arrays::BoolArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::VarBinViewArray; + use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt; use vortex_array::dtype::DType; use vortex_array::dtype::Nullability::Nullable; use vortex_array::scalar::Scalar; + use vortex_array::scalar_fn::EmptyOptions; + use vortex_array::scalar_fn::fns::mask::Mask; use vortex_buffer::buffer; use vortex_error::VortexResult; @@ -224,4 +243,23 @@ mod tests { assert_eq!(expected.as_ref(), actual.as_ref()); Ok(()) } + + #[test] + fn mask_wrapped_dict_exports() -> VortexResult<()> { + // Dictionary behind a lazy `mask` scalar-fn — the shape a scan produces when a row + // mask is applied to a dict-encoded column. + let dict = DictArray::try_new( + buffer![0u8, 1, 0].into_array(), + VarBinViewArray::from_iter_str(["a", "b"]).into_array(), + )?; + let mask = BoolArray::from_iter([true, false, true]); + let masked = Mask.try_new_array(3, EmptyOptions, [dict.into_array(), mask.into_array()])?; + + let actual = execute(masked, &dict_type(DataType::UInt8, DataType::Utf8))?; + let flat = arrow_cast::cast(&actual, &DataType::Utf8)?; + + let expected = StringArray::from(vec![Some("a"), None, Some("a")]); + assert_eq!(flat.as_ref(), &expected as &dyn arrow_array::Array); + Ok(()) + } } diff --git a/vortex-arrow/src/executor/list.rs b/vortex-arrow/src/executor/list.rs index 145c8dfc80c..271fedd5667 100644 --- a/vortex-arrow/src/executor/list.rs +++ b/vortex-arrow/src/executor/list.rs @@ -25,6 +25,7 @@ use vortex_array::builtins::ArrayBuiltins; use vortex_array::dtype::DType; use vortex_array::dtype::NativePType; use vortex_array::dtype::Nullability; +use vortex_array::matcher::Matcher; use vortex_buffer::BufferMut; use vortex_error::VortexExpect; use vortex_error::VortexResult; @@ -33,6 +34,17 @@ use vortex_error::vortex_ensure; use crate::executor::validity::to_arrow_null_buffer; use crate::session::ArrowSessionExt; +/// Matches the encodings [`to_arrow_list`] requires for export. +struct ArrowListExportable; + +impl Matcher for ArrowListExportable { + type Match<'a> = &'a ArrayRef; + + fn try_match(array: &ArrayRef) -> Option> { + (array.is::() || array.is::() || array.is::()).then_some(array) + } +} + #[allow(rustdoc::broken_intra_doc_links)] /// Convert a Vortex VarBinArray into an Arrow [`GenericListArray`](arrow_array:array::GenericListArray). pub(super) fn to_arrow_list( @@ -40,9 +52,11 @@ pub(super) fn to_arrow_list( elements_field: &FieldRef, ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = array.execute_until::(ctx)?; + // If the Vortex array is already in List format, we can directly convert it. - if let Some(array) = array.as_opt::() { - return list_to_list::(&array.into_owned(), elements_field, ctx); + if let Some(list) = array.as_opt::() { + return list_to_list::(&list.into_owned(), elements_field, ctx); } // Converting each chunk individually, then using the fast concat logic from arrow @@ -56,24 +70,15 @@ pub(super) fn to_arrow_list( return Ok(arrow_select::concat::concat(&refs)?); } - // If the Vortex array is a ListViewArray, rebuild to ZCTL if needed and convert. - let array = match array.try_downcast::() { - Ok(array) => { - let zctl = if array.is_zero_copy_to_list() { - array - } else { - array.rebuild(ListViewRebuildMode::MakeZeroCopyToList, ctx)? - }; - return list_view_zctl::(zctl, elements_field, ctx); - } - Err(a) => a, - }; - - // Otherwise, we execute the array to become a ListViewArray, then rebuild to ZCTL. + // Otherwise the array is canonical: a ListViewArray, which we rebuild to ZCTL if needed. // Note: arrow_cast::cast supports ListView → List (apache/arrow-rs#8735), but it // unconditionally uses take. Our rebuild uses a heuristic that picks list-by-list // for large lists, which avoids materializing a large index buffer. - let list_view = array.execute::(ctx)?; + let list_view = array + .as_opt::() + .vortex_expect("Must be ListView from Matcher") + .into_owned(); + let zctl = if list_view.is_zero_copy_to_list() { list_view } else { @@ -213,15 +218,21 @@ mod tests { use vortex_array::Canonical; use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; + use vortex_array::arrays::BoolArray; use vortex_array::arrays::PrimitiveArray; + use vortex_array::arrays::SliceArray; + use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt; use vortex_array::dtype::DType; use vortex_array::dtype::Nullability::NonNullable; + use vortex_array::scalar_fn::EmptyOptions; + use vortex_array::scalar_fn::fns::mask::Mask; use vortex_array::validity::Validity; use vortex_buffer::buffer; use vortex_error::VortexResult; use vortex_session::VortexSession; use crate::ArrowArrayExecutor; + use crate::executor::list::ListArray; use crate::executor::list::ListViewArray; /// A shared session for these list-executor tests, used to create execution contexts. @@ -364,6 +375,92 @@ mod tests { Ok(()) } + #[test] + fn slice_wrapped_list_exports() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + // Lists [[1, 2], [3], [4, 5, 6]] behind a lazy Slice wrapper selecting rows 1..3. + let elements = PrimitiveArray::new(buffer![1i32, 2, 3, 4, 5, 6], Validity::NonNullable); + let elements_ptr = elements.as_slice::().as_ptr(); + let offsets = PrimitiveArray::new(buffer![0i32, 2, 3, 6], Validity::NonNullable); + let list = ListArray::try_new( + elements.into_array(), + offsets.into_array(), + Validity::NonNullable, + )?; + let sliced = SliceArray::new(list.into_array(), 1..3).into_array(); + + let field = Field::new("item", DataType::Int32, false); + let arrow_dt = DataType::List(field.into()); + let arrow_array = sliced.execute_arrow(Some(&arrow_dt), &mut ctx)?; + + let arrow_list = arrow_array + .as_any() + .downcast_ref::>() + .unwrap(); + assert_eq!(arrow_list.len(), 2); + let first = arrow_list.value(0); + let first_vals = first.as_any().downcast_ref::().unwrap(); + assert_eq!(first_vals.values(), &[3]); + let second = arrow_list.value(1); + let second_vals = second.as_any().downcast_ref::().unwrap(); + assert_eq!(second_vals.values(), &[4, 5, 6]); + + // The conversion shares the elements buffer regardless of which path resolves the + // Slice (revealed List or zero-copy-to-list ListView). + let values = arrow_list + .values() + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(values.values().as_ptr(), elements_ptr); + Ok(()) + } + + #[test] + fn mask_wrapped_list_exports() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + // Lists [[1, 2], [3], [4, 5, 6]] behind a lazy `mask` scalar-fn nulling out row 1 — + // the shape a scan produces when a row mask is applied to a List-encoded column. + let elements = PrimitiveArray::new(buffer![1i32, 2, 3, 4, 5, 6], Validity::NonNullable); + let elements_ptr = elements.as_slice::().as_ptr(); + let offsets = PrimitiveArray::new(buffer![0i32, 2, 3, 6], Validity::NonNullable); + let list = ListArray::try_new( + elements.into_array(), + offsets.into_array(), + Validity::NonNullable, + )?; + let mask = BoolArray::from_iter([true, false, true]); + let masked = Mask.try_new_array(3, EmptyOptions, [list.into_array(), mask.into_array()])?; + + let field = Field::new("item", DataType::Int32, false); + let arrow_dt = DataType::List(field.into()); + let arrow_array = masked.execute_arrow(Some(&arrow_dt), &mut ctx)?; + + let arrow_list = arrow_array + .as_any() + .downcast_ref::>() + .unwrap(); + assert_eq!(arrow_list.len(), 3); + assert!(!arrow_list.is_null(0)); + assert!(arrow_list.is_null(1)); + assert!(!arrow_list.is_null(2)); + let first = arrow_list.value(0); + let first_vals = first.as_any().downcast_ref::().unwrap(); + assert_eq!(first_vals.values(), &[1, 2]); + let third = arrow_list.value(2); + let third_vals = third.as_any().downcast_ref::().unwrap(); + assert_eq!(third_vals.values(), &[4, 5, 6]); + + // Masking only touches validity, so the conversion still shares the elements buffer. + let values = arrow_list + .values() + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(values.values().as_ptr(), elements_ptr); + Ok(()) + } + #[test] fn test_to_arrow_list_empty_zctl() -> VortexResult<()> { let mut ctx = SESSION.create_execution_ctx(); diff --git a/vortex-arrow/src/executor/run_end.rs b/vortex-arrow/src/executor/run_end.rs index a2be95b3b21..1c6b2e04145 100644 --- a/vortex-arrow/src/executor/run_end.rs +++ b/vortex-arrow/src/executor/run_end.rs @@ -16,6 +16,7 @@ use vortex_array::ExecutionCtx; use vortex_array::IntoArray; use vortex_array::arrays::Constant; use vortex_array::arrays::ConstantArray; +use vortex_array::matcher::Matcher; use vortex_error::VortexError; use vortex_error::VortexResult; use vortex_error::vortex_bail; @@ -27,34 +28,41 @@ use vortex_runend::RunEndArraySlotsExt; use crate::ArrowArrayExecutor; +/// Matches the encodings [`to_arrow_run_end`] requires for export. +struct ArrowRunEndExportable; + +impl Matcher for ArrowRunEndExportable { + type Match<'a> = &'a ArrayRef; + + fn try_match(array: &ArrayRef) -> Option> { + (array.is::() || array.is::()).then_some(array) + } +} + pub(super) fn to_arrow_run_end( array: ArrayRef, ends_type: &DataType, values_type: &Field, ctx: &mut ExecutionCtx, ) -> VortexResult { + let array = array.execute_until::(ctx)?; + let array = match array.try_downcast::() { - Ok(constant) => { - return constant_to_run_end(constant, ends_type, values_type, ctx); - } + Ok(constant) => return constant_to_run_end(constant, ends_type, values_type, ctx), + Err(array) => array, + }; + let array = match array.try_downcast::() { + Ok(run_end) => return run_end_to_arrow(run_end, ends_type, values_type, ctx), Err(array) => array, }; - // Execute to unwrap any wrapper VTables (Slice, Filter, etc.) which may - // reveal a RunEndArray. - let array = array.execute::(ctx)?; - match array.try_downcast::() { - Ok(run_end) => run_end_to_arrow(run_end, ends_type, values_type, ctx), - Err(array) => { - // Fallback: canonicalize to flat Arrow, then cast to REE. - let flat = array.execute_arrow(Some(values_type.data_type()), ctx)?; - let ree_type = DataType::RunEndEncoded( - Arc::new(Field::new("run_ends", ends_type.clone(), false)), - Arc::new(values_type.clone()), - ); - arrow_cast::cast(&flat, &ree_type).map_err(VortexError::from) - } - } + // Fallback: canonicalize to flat Arrow, then cast to REE. + let flat = array.execute_arrow(Some(values_type.data_type()), ctx)?; + let ree_type = DataType::RunEndEncoded( + Arc::new(Field::new("run_ends", ends_type.clone(), false)), + Arc::new(values_type.clone()), + ); + arrow_cast::cast(&flat, &ree_type).map_err(VortexError::from) } fn run_end_to_arrow( @@ -178,18 +186,26 @@ mod tests { use vortex_array::IntoArray; use vortex_array::VortexSessionExecute; use vortex_array::arrays::PrimitiveArray; + use vortex_array::arrays::SliceArray; use vortex_array::dtype::DType; use vortex_array::dtype::Nullability::Nullable; use vortex_array::dtype::PType; use vortex_array::scalar::Scalar; + use vortex_array::validity::Validity; + use vortex_buffer::buffer; use vortex_error::VortexResult; use vortex_error::vortex_err; use vortex_session::VortexSession; use crate::ArrowArrayExecutor; use crate::executor::run_end::ConstantArray; + use crate::executor::run_end::RunEnd; - static SESSION: LazyLock = LazyLock::new(vortex_array::array_session); + static SESSION: LazyLock = LazyLock::new(|| { + let session = vortex_array::array_session(); + vortex_runend::initialize(&session); + session + }); fn ree_type(ends: DataType, values_dtype: DataType) -> DataType { DataType::RunEndEncoded( @@ -262,6 +278,38 @@ mod tests { Ok(()) } + #[test] + fn nested_wrappers_reveal_run_end_fast_path() -> VortexResult<()> { + let mut ctx = SESSION.create_execution_ctx(); + // Runs [7, 7, 8, 8] behind two nested lazy Slice wrappers. A single execution step + // only merges the slices, so the exporter must keep stepping to reveal the + // RunEndArray underneath. + let ends = PrimitiveArray::new(buffer![2u32, 4], Validity::NonNullable); + let values = PrimitiveArray::new(buffer![7i64, 8], Validity::NonNullable); + let values_ptr = values.as_slice::().as_ptr(); + let ree = RunEnd::try_new(ends.into_array(), values.into_array(), &mut ctx)?; + let wrapped = SliceArray::new(SliceArray::new(ree.into_array(), 0..4).into_array(), 0..4) + .into_array(); + + let result = execute(wrapped, &ree_type(DataType::Int32, DataType::Int64))?; + + let ree_arrow = result + .as_any() + .downcast_ref::>() + .ok_or_else(|| vortex_err!("expected Int32 run-end array"))?; + assert_eq!(ree_arrow.run_ends().values(), &[2, 4]); + + // The RunEnd fast path exports the values buffer zero-copy; the flat-then-recode + // fallback would materialize new buffers. + let arrow_values = ree_arrow + .values() + .as_any() + .downcast_ref::() + .ok_or_else(|| vortex_err!("expected Int64 values"))?; + assert_eq!(arrow_values.values().as_ptr(), values_ptr); + Ok(()) + } + #[test] fn primitive_to_ree() -> VortexResult<()> { let array = PrimitiveArray::from_iter(vec![10i32, 10, 20, 20, 20]).into_array(); diff --git a/vortex-arrow/src/executor/struct_.rs b/vortex-arrow/src/executor/struct_.rs index e2ce2700d4a..f5bd3a92e4d 100644 --- a/vortex-arrow/src/executor/struct_.rs +++ b/vortex-arrow/src/executor/struct_.rs @@ -22,6 +22,7 @@ use vortex_array::builtins::ArrayBuiltins; use vortex_array::dtype::DType; use vortex_array::dtype::FieldNames; use vortex_array::dtype::StructFields; +use vortex_array::matcher::Matcher; use vortex_array::scalar_fn::fns::pack::Pack; use vortex_error::VortexResult; use vortex_error::vortex_ensure; @@ -31,6 +32,22 @@ use crate::dtype::FromArrowType; use crate::executor::validity::to_arrow_null_buffer; use crate::session::ArrowSessionExt; +/// Matches the encodings [`to_arrow_struct`] requires for export. +struct ArrowStructExportable; + +impl Matcher for ArrowStructExportable { + type Match<'a> = &'a ArrayRef; + + fn try_match(array: &ArrayRef) -> Option> { + (array.is::() + || array.is::() + || array + .as_opt::() + .is_some_and(|scalar_fn| scalar_fn.scalar_fn().as_opt::().is_some())) + .then_some(array) + } +} + pub(super) fn to_arrow_struct( array: ArrayRef, target_fields: Option<&Fields>, @@ -38,6 +55,8 @@ pub(super) fn to_arrow_struct( ) -> VortexResult { let len = array.len(); + let array = array.execute_until::(ctx)?; + // If the array is chunked, then we invert the chunk-of-struct to struct-of-chunk. let array = match array.try_downcast::() { Ok(array) => { @@ -196,6 +215,7 @@ mod tests { use std::sync::Arc; use arrays::varbinview::VarBinViewArray; + use arrow_array::Array; use arrow_array::ArrayRef; use arrow_array::PrimitiveArray as ArrowPrimitiveArray; use arrow_array::StringViewArray; @@ -209,9 +229,13 @@ mod tests { use vortex_array::VortexSessionExecute; use vortex_array::array_session; use vortex_array::arrays; + use vortex_array::arrays::BoolArray; use vortex_array::arrays::PrimitiveArray; use vortex_array::arrays::StructArray; + use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt; use vortex_array::dtype::FieldNames; + use vortex_array::scalar_fn::EmptyOptions; + use vortex_array::scalar_fn::fns::mask::Mask; use vortex_array::validity::Validity; use vortex_buffer::buffer; use vortex_error::VortexResult; @@ -339,6 +363,44 @@ mod tests { Ok(()) } + #[test] + fn mask_wrapped_struct_exports_via_struct_fast_path() -> VortexResult<()> { + let mut ctx = array_session().create_execution_ctx(); + // A struct behind a lazy `mask` scalar-fn nulling out row 1 — the shape a scan + // produces when a row mask is applied to a top-level struct batch. + let xs = PrimitiveArray::new(buffer![1i64, 2, 3], Validity::NonNullable); + let struct_array = StructArray::try_new( + FieldNames::from(["xs"]), + vec![xs.into_array()], + 3, + Validity::NonNullable, + )?; + let mask = BoolArray::from_iter([true, false, true]); + let masked = Mask.try_new_array( + 3, + EmptyOptions, + [struct_array.into_array(), mask.into_array()], + )?; + + let arrow = masked.execute_arrow(None, &mut ctx)?; + + let arrow_struct = arrow + .as_any() + .downcast_ref::() + .expect("struct array"); + assert_eq!(arrow_struct.len(), 3); + assert!(!arrow_struct.is_null(0)); + assert!(arrow_struct.is_null(1)); + assert!(!arrow_struct.is_null(2)); + let xs_col = arrow_struct + .column(0) + .as_any() + .downcast_ref::>() + .expect("int64 column"); + assert_eq!(xs_col.values(), &[1, 2, 3]); + Ok(()) + } + #[test] fn to_arrow_with_non_nullable_fields() -> VortexResult<()> { let mut ctx = array_session().create_execution_ctx();