1use std::any::Any;
24use std::borrow::Cow;
25use std::fmt::Debug;
26use std::sync::Arc;
27
28use arrow_array::Array as ArrowArray;
29use arrow_array::ArrayRef as ArrowArrayRef;
30use arrow_array::RecordBatch;
31use arrow_array::RunArray;
32use arrow_array::make_array;
33use arrow_array::types::Int16Type;
34use arrow_array::types::Int32Type;
35use arrow_array::types::Int64Type;
36use arrow_array::types::RunEndIndexType;
37use arrow_schema::DataType;
38use arrow_schema::Field;
39use arrow_schema::FieldRef;
40use arrow_schema::Fields;
41use arrow_schema::Schema;
42use arrow_schema::extension::EXTENSION_TYPE_NAME_KEY;
43use tracing::debug;
44use tracing::trace;
45use vortex_array::ArrayRef;
46use vortex_array::ExecutionCtx;
47use vortex_array::IntoArray;
48use vortex_array::arrays::DictArray;
49use vortex_array::arrays::FixedSizeListArray;
50use vortex_array::arrays::ListArray;
51use vortex_array::arrays::ListViewArray;
52use vortex_array::arrays::StructArray;
53use vortex_array::dtype::DType;
54use vortex_array::dtype::FieldName;
55use vortex_array::dtype::FieldNames;
56use vortex_array::dtype::NativePType;
57use vortex_array::dtype::Nullability;
58use vortex_array::dtype::StructFields;
59use vortex_array::dtype::extension::ExtId;
60use vortex_array::extension::datetime::AnyTemporal;
61use vortex_array::validity::Validity;
62use vortex_error::VortexResult;
63use vortex_error::vortex_bail;
64use vortex_error::vortex_ensure;
65use vortex_error::vortex_ensure_eq;
66use vortex_error::vortex_err;
67use vortex_session::ArcSwapMap;
68use vortex_session::SessionExt;
69use vortex_session::SessionGuard;
70use vortex_session::SessionVar;
71use vortex_session::registry::Id;
72
73use crate::ArrowExportOptions;
74use crate::IntoVortexArray;
75use crate::convert::from_arrow_dyn;
76use crate::convert::map_from_arrow_parts;
77use crate::convert::nulls;
78use crate::convert::remove_nulls;
79use crate::convert::trim_offsets;
80use crate::dtype::from_arrow_data_type;
81use crate::dtype::to_data_type_naive;
82use crate::executor::execute_arrow_naive;
83use crate::run_end_import::run_end_from_arrow;
84
85pub enum ArrowExport {
91 Unsupported(ArrayRef),
93 Exported(ArrowArrayRef),
95}
96
97pub enum ArrowImport {
103 Unsupported(ArrowArrayRef),
105 Imported(ArrayRef),
107}
108
109pub trait IntoArrowField<'a> {
122 fn into_arrow_field(self, data_type: &DataType) -> Cow<'a, Field>;
124}
125
126impl<'a> IntoArrowField<'a> for &'a Field {
127 fn into_arrow_field(self, _data_type: &DataType) -> Cow<'a, Field> {
128 Cow::Borrowed(self)
129 }
130}
131
132impl<'a> IntoArrowField<'a> for &'a FieldRef {
133 fn into_arrow_field(self, _data_type: &DataType) -> Cow<'a, Field> {
134 Cow::Borrowed(self.as_ref())
135 }
136}
137
138impl<'a> IntoArrowField<'a> for bool {
139 fn into_arrow_field(self, data_type: &DataType) -> Cow<'a, Field> {
140 Cow::Owned(Field::new("", data_type.clone(), self))
141 }
142}
143
144impl<'a> IntoArrowField<'a> for Nullability {
145 fn into_arrow_field(self, data_type: &DataType) -> Cow<'a, Field> {
146 self.is_nullable().into_arrow_field(data_type)
147 }
148}
149
150pub trait ArrowExportVTable: 'static + Send + Sync + Debug {
155 fn arrow_ext_id(&self) -> Id;
157
158 fn vortex_id(&self) -> Id;
162
163 fn to_arrow_field(
166 &self,
167 name: &str,
168 dtype: &DType,
169 session: &ArrowSession,
170 ) -> VortexResult<Option<Field>>;
171
172 fn execute_arrow(
178 &self,
179 array: ArrayRef,
180 target: &Field,
181 options: &ArrowExportOptions,
182 ctx: &mut ExecutionCtx,
183 ) -> VortexResult<ArrowExport>;
184}
185
186pub trait ArrowImportVTable: 'static + Send + Sync + Debug {
193 fn arrow_ext_id(&self) -> Id;
195
196 #[allow(clippy::wrong_self_convention)]
203 fn from_arrow_field(
204 &self,
205 field: &Field,
206 session: &ArrowSession,
207 ) -> VortexResult<Option<DType>>;
208
209 #[allow(clippy::wrong_self_convention)]
218 fn from_arrow_array(
219 &self,
220 array: ArrowArrayRef,
221 field: &Field,
222 dtype: &DType,
223 session: &ArrowSession,
224 ) -> VortexResult<ArrowImport>;
225}
226
227pub type ArrowExportVTableRef = Arc<dyn ArrowExportVTable>;
228pub type ArrowImportVTableRef = Arc<dyn ArrowImportVTable>;
229
230type ArrowExporterRegistry = ArcSwapMap<Id, Arc<[ArrowExportVTableRef]>>;
232type VortexExporterRegistry = ArcSwapMap<ExtId, Arc<[ArrowExportVTableRef]>>;
234type ArrowImporterRegistry = ArcSwapMap<Id, Arc<[ArrowImportVTableRef]>>;
236
237#[derive(Clone, Debug, Default)]
247pub struct ArrowSession {
248 exporters: ArrowExporterRegistry,
249 exporters_by_vortex: VortexExporterRegistry,
250 importers: ArrowImporterRegistry,
251}
252
253impl ArrowSession {
254 pub fn register_exporter(&self, exporter: ArrowExportVTableRef) {
257 self.exporters.push(
258 exporter.arrow_ext_id(),
259 ArrowExportVTableRef::clone(&exporter),
260 );
261 self.exporters_by_vortex
262 .push(exporter.vortex_id(), exporter);
263 }
264
265 pub fn register_importer(&self, importer: ArrowImportVTableRef) {
267 self.importers.push(importer.arrow_ext_id(), importer);
268 }
269
270 fn exporters(&self, id: &Id) -> Arc<[ArrowExportVTableRef]> {
271 self.exporters.get(id).unwrap_or_else(|| Arc::from([]))
272 }
273
274 fn exporters_by_vortex(&self, id: &Id) -> Arc<[ArrowExportVTableRef]> {
275 self.exporters_by_vortex
276 .get(id)
277 .unwrap_or_else(|| Arc::from([]))
278 }
279
280 fn importers(&self, id: &Id) -> Arc<[ArrowImportVTableRef]> {
281 self.importers.get(id).unwrap_or_else(|| Arc::from([]))
282 }
283
284 pub fn to_arrow_field(&self, name: &str, dtype: &DType) -> VortexResult<Field> {
289 match dtype {
291 DType::List(elem_dtype, nullability) => {
292 let elem_field = self.to_arrow_field(Field::LIST_FIELD_DEFAULT_NAME, elem_dtype)?;
293 Ok(Field::new_list(name, elem_field, nullability.is_nullable()))
294 }
295 DType::FixedSizeList(elem_dtype, elem_size, nullability) => {
296 let elem_field = self.to_arrow_field(Field::LIST_FIELD_DEFAULT_NAME, elem_dtype)?;
297 Ok(Field::new_fixed_size_list(
298 name,
299 elem_field,
300 (*elem_size).try_into()?,
301 nullability.is_nullable(),
302 ))
303 }
304 DType::Map(map_dtype, nullability) => {
305 let key = self.to_arrow_field("key", &map_dtype.key_dtype())?;
306 let value = self.to_arrow_field("value", &map_dtype.value_dtype())?;
307 let entries = Field::new_struct("entries", Fields::from(vec![key, value]), false);
308 Ok(Field::new(
309 name,
310 DataType::Map(Arc::new(entries), map_dtype.keys_sorted()),
311 nullability.is_nullable(),
312 ))
313 }
314 DType::Struct(fields, nullability) => {
315 let arrow_fields = Fields::from_iter(
316 fields
317 .fields()
318 .zip(fields.names().iter())
319 .map(|(field, name)| self.to_arrow_field(name.as_ref(), &field))
320 .collect::<VortexResult<Vec<_>>>()?,
321 );
322 Ok(Field::new_struct(
323 name,
324 arrow_fields,
325 nullability.is_nullable(),
326 ))
327 }
328 DType::Extension(ext) if !ext.is::<AnyTemporal>() => {
329 for plugin in self.exporters_by_vortex(&ext.id()).iter() {
330 if let Some(field) =
331 plugin.to_arrow_field(name, &DType::Extension(ext.clone()), self)?
332 {
333 return Ok(field);
334 }
335 }
336 vortex_bail!("extension type cannot be converted to Arrow without a plugin: {ext}");
337 }
338 DType::Variant(_) => {
339 Ok(Field::new(
343 name,
344 DataType::Struct(
345 vec![
346 Field::new("metadata", DataType::BinaryView, dtype.is_nullable()),
347 Field::new("value", DataType::BinaryView, dtype.is_nullable()),
348 ]
349 .into(),
350 ),
351 dtype.is_nullable(),
352 )
353 .with_metadata(
354 [(
355 EXTENSION_TYPE_NAME_KEY.to_string(),
356 "arrow.parquet.variant".to_string(),
357 )]
358 .into(),
359 ))
360 }
361 _ => Ok(Field::new(
362 name,
363 to_data_type_naive(dtype)?,
364 dtype.is_nullable(),
365 )),
366 }
367 }
368
369 pub fn to_arrow_schema(&self, dtype: &DType) -> VortexResult<Schema> {
373 let DType::Struct(struct_dtype, _) = dtype else {
374 vortex_bail!("to_arrow_schema requires a top-level struct dtype, got {dtype}");
375 };
376 let mut fields = Vec::with_capacity(struct_dtype.names().len());
377 for (name, field_dtype) in struct_dtype.names().iter().zip(struct_dtype.fields()) {
378 fields.push(self.to_arrow_field(name.as_ref(), &field_dtype)?);
379 }
380 Ok(Schema::new(fields))
381 }
382
383 pub fn to_arrow_datatype(&self, dtype: &DType) -> VortexResult<DataType> {
389 Ok(self.to_arrow_field("", dtype)?.data_type().clone())
390 }
391
392 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
401 pub fn from_arrow_field(&self, field: &Field) -> VortexResult<DType> {
402 if let Some(name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
403 for plugin in self.importers(&Id::new(name)).iter() {
404 if let Some(dtype) = plugin.from_arrow_field(field, self)? {
405 return Ok(dtype);
406 }
407 }
408 if name == "arrow.parquet.variant" {
410 return Ok(DType::Variant(field.is_nullable().into()));
411 }
412 }
413 if let DataType::Dictionary(_, value_type) = field.data_type() {
414 return self
415 .from_arrow_field(&field.clone().with_data_type(value_type.as_ref().clone()));
416 }
417 self.from_arrow_datatype(field.data_type(), field.is_nullable().into())
418 }
419
420 pub fn from_arrow_datatype(
431 &self,
432 data_type: &DataType,
433 nullability: Nullability,
434 ) -> VortexResult<DType> {
435 Ok(match data_type {
436 DataType::List(elem)
437 | DataType::LargeList(elem)
438 | DataType::ListView(elem)
439 | DataType::LargeListView(elem) => {
440 DType::List(Arc::new(self.from_arrow_field(elem.as_ref())?), nullability)
441 }
442 DataType::FixedSizeList(elem, size) => DType::FixedSizeList(
443 Arc::new(self.from_arrow_field(elem.as_ref())?),
444 *size as u32,
445 nullability,
446 ),
447 DataType::Map(entries, keys_sorted) => {
448 vortex_ensure!(
449 !entries.is_nullable(),
450 "Arrow map entries field must be non-nullable"
451 );
452 let DataType::Struct(fields) = entries.data_type() else {
453 vortex_bail!(
454 "Arrow map entries field must have Struct type, got {:?}",
455 entries.data_type()
456 );
457 };
458 vortex_ensure_eq!(
459 fields.len(),
460 2,
461 "Arrow map entries struct must contain exactly two fields"
462 );
463 vortex_ensure!(
464 !fields[0].is_nullable(),
465 "Arrow map key field must be non-nullable"
466 );
467 DType::map(
468 self.from_arrow_field(fields[0].as_ref())?,
469 self.from_arrow_field(fields[1].as_ref())?,
470 *keys_sorted,
471 nullability,
472 )?
473 }
474 DataType::Struct(fields) => DType::Struct(self.from_arrow_fields(fields)?, nullability),
475 DataType::Dictionary(_, value_type) => {
476 self.from_arrow_datatype(value_type.as_ref(), nullability)?
477 }
478 DataType::RunEndEncoded(_, value_field) => {
479 self.from_arrow_field(&run_end_values_field(value_field, nullability))?
480 }
481 _ => from_arrow_data_type(data_type, nullability)?,
482 })
483 }
484
485 pub fn from_arrow_fields(&self, fields: &Fields) -> VortexResult<StructFields> {
488 fields
489 .iter()
490 .map(|f| {
491 self.from_arrow_field(f)
492 .map(|dt| (FieldName::from(f.name().as_str()), dt))
493 })
494 .collect::<VortexResult<StructFields>>()
495 }
496
497 pub fn from_arrow_schema(&self, schema: &Schema) -> VortexResult<DType> {
501 Ok(DType::Struct(
502 self.from_arrow_fields(schema.fields())?,
503 Nullability::NonNullable,
504 ))
505 }
506
507 pub fn from_arrow_record_batch(
515 &self,
516 batch: RecordBatch,
517 schema: &Schema,
518 ) -> VortexResult<ArrayRef> {
519 vortex_ensure_eq!(batch.num_columns(), schema.fields().len());
520 let length = batch.num_rows();
521 let names = FieldNames::from_iter(
522 schema
523 .fields()
524 .iter()
525 .map(|f| FieldName::from(f.name().as_str())),
526 );
527 let mut columns = Vec::with_capacity(schema.fields().len());
528 for (col, field) in batch.columns().iter().zip(schema.fields().iter()) {
529 columns.push(self.from_arrow_array_inner(ArrowArrayRef::clone(col), field)?);
530 }
531 Ok(StructArray::try_new(names, columns, length, Validity::NonNullable)?.into_array())
532 }
533
534 pub fn execute_arrow(
545 &self,
546 array: ArrayRef,
547 target: Option<&Field>,
548 ctx: &mut ExecutionCtx,
549 ) -> VortexResult<ArrowArrayRef> {
550 self.exporter(&ArrowExportOptions::default())
551 .execute_arrow(array, target, ctx)
552 }
553
554 pub fn exporter<'a>(&'a self, options: &'a ArrowExportOptions) -> ArrowExporter<'a> {
578 ArrowExporter {
579 session: self,
580 options,
581 }
582 }
583
584 pub fn from_arrow_array<'a>(
604 &self,
605 array: ArrowArrayRef,
606 field: impl IntoArrowField<'a>,
607 ) -> VortexResult<ArrayRef> {
608 let field = field.into_arrow_field(array.data_type());
609 self.from_arrow_array_inner(array, field.as_ref())
610 }
611
612 #[allow(clippy::wrong_self_convention)]
616 fn from_arrow_array_inner(
617 &self,
618 array: ArrowArrayRef,
619 field: &Field,
620 ) -> VortexResult<ArrayRef> {
621 if let Some(extension_name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
622 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
623 let importers = self.importers(&Id::new(extension_name));
624 if !importers.is_empty() {
625 let dtype = self.from_arrow_field(field)?;
626 let len = array.len();
627 let mut current = array;
628 for plugin in importers.iter() {
629 match plugin.from_arrow_array(current, field, &dtype, self)? {
630 ArrowImport::Imported(arr) => {
634 vortex_ensure_eq!(
635 arr.len(),
636 len,
637 "Arrow importer {} returned {} elements for an array of {}",
638 plugin.arrow_ext_id(),
639 arr.len(),
640 len
641 );
642 return Ok(arr);
643 }
644 ArrowImport::Unsupported(arr) => {
645 vortex_ensure_eq!(
646 arr.len(),
647 len,
648 "Arrow importer {} declined and returned {} elements for an array of {}",
649 plugin.arrow_ext_id(),
650 arr.len(),
651 len
652 );
653 current = arr;
654 }
655 }
656 }
657 return self.from_arrow_array_canonical(current.as_ref(), field);
658 }
659 }
660 self.from_arrow_array_canonical(array.as_ref(), field)
661 }
662
663 #[allow(clippy::wrong_self_convention)]
666 fn from_arrow_array_canonical(
667 &self,
668 array: &dyn ArrowArray,
669 field: &Field,
670 ) -> VortexResult<ArrayRef> {
671 use arrow_array::cast::AsArray;
672
673 match field.data_type() {
674 DataType::Struct(fields) => {
675 let arrow_struct = array.as_struct();
676 let names = FieldNames::from_iter(
677 fields.iter().map(|f| FieldName::from(f.name().as_str())),
678 );
679 let columns = arrow_struct
680 .columns()
681 .iter()
682 .zip(fields.iter())
683 .map(|(col, child_field)| {
684 let inner = if col.null_count() > 0 && !child_field.is_nullable() {
687 make_array(remove_nulls(col.to_data())?)
688 } else {
689 ArrowArrayRef::clone(col)
690 };
691 self.from_arrow_array_inner(inner, child_field.as_ref())
692 })
693 .collect::<VortexResult<Vec<_>>>()?;
694 let validity = nulls(arrow_struct.nulls(), field.is_nullable())?;
695 Ok(
696 StructArray::try_new(names, columns, arrow_struct.len(), validity)?
697 .into_array(),
698 )
699 }
700 DataType::List(elem_field) => {
701 let list = array.as_list::<i32>();
702 let (offsets, referenced) = trim_offsets(list.offsets());
703 let elements = self.from_arrow_array(
704 list.values().slice(referenced.start, referenced.len()),
705 elem_field.as_ref(),
706 )?;
707 let validity = nulls(list.nulls(), field.is_nullable())?;
708 Ok(
711 unsafe { ListArray::new_unchecked(elements, offsets.into_array(), validity) }
712 .into_array(),
713 )
714 }
715 DataType::LargeList(elem_field) => {
716 let list = array.as_list::<i64>();
717 let (offsets, referenced) = trim_offsets(list.offsets());
718 let elements = self.from_arrow_array(
719 list.values().slice(referenced.start, referenced.len()),
720 elem_field.as_ref(),
721 )?;
722 let validity = nulls(list.nulls(), field.is_nullable())?;
723 Ok(
726 unsafe { ListArray::new_unchecked(elements, offsets.into_array(), validity) }
727 .into_array(),
728 )
729 }
730 DataType::FixedSizeList(elem_field, list_size) => {
731 let fsl = array.as_fixed_size_list();
732 let elements = self.from_arrow_array_inner(
733 ArrowArrayRef::clone(fsl.values()),
734 elem_field.as_ref(),
735 )?;
736 let validity = nulls(fsl.nulls(), field.is_nullable())?;
737 Ok(
738 FixedSizeListArray::try_new(elements, *list_size as u32, validity, fsl.len())?
739 .into_array(),
740 )
741 }
742 DataType::ListView(elem_field) => {
743 let list = array.as_list_view::<i32>();
744 let elements = self
745 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
746 let offsets = list.offsets().clone().into_array();
747 let sizes = list.sizes().clone().into_array();
748 let validity = nulls(list.nulls(), field.is_nullable())?;
749 Ok(
752 unsafe { ListViewArray::new_unchecked(elements, offsets, sizes, validity) }
753 .into_array(),
754 )
755 }
756 DataType::LargeListView(elem_field) => {
757 let list = array.as_list_view::<i64>();
758 let elements = self
759 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
760 let offsets = list.offsets().clone().into_array();
761 let sizes = list.sizes().clone().into_array();
762 let validity = nulls(list.nulls(), field.is_nullable())?;
763 Ok(
766 unsafe { ListViewArray::new_unchecked(elements, offsets, sizes, validity) }
767 .into_array(),
768 )
769 }
770 DataType::Map(entries_field, keys_sorted) => {
771 let map = array.as_map();
772 let entries_array: ArrowArrayRef = Arc::new(map.entries().clone());
773 let entries = self.from_arrow_array_inner(entries_array, entries_field.as_ref())?;
774 map_from_arrow_parts(
775 entries,
776 map.offsets(),
777 map.nulls(),
778 *keys_sorted,
779 field.is_nullable(),
780 )
781 }
782 DataType::RunEndEncoded(ends_field, values_field) => {
783 let values_field = run_end_values_field(values_field, field.is_nullable().into());
784 match ends_field.data_type() {
785 DataType::Int16 => self.run_end_from_arrow::<Int16Type>(array, &values_field),
786 DataType::Int32 => self.run_end_from_arrow::<Int32Type>(array, &values_field),
787 DataType::Int64 => self.run_end_from_arrow::<Int64Type>(array, &values_field),
788 ends_dt => vortex_bail!(
789 "Arrow run-end array run ends must be Int16, Int32 or Int64, got {ends_dt}"
790 ),
791 }
792 }
793 DataType::Dictionary(..) => {
794 let dict = array.as_any_dictionary();
795 let values_field = field
797 .clone()
798 .with_data_type(dict.values().data_type().clone());
799 let values =
800 self.from_arrow_array(ArrowArrayRef::clone(dict.values()), &values_field)?;
801 let codes = dict.keys();
802 let codes = from_arrow_dyn(codes, codes.is_nullable())?;
803 Ok(unsafe { DictArray::new_unchecked(codes, values) }.into_array())
806 }
807 _ => from_arrow_dyn(array, field.is_nullable()),
808 }
809 }
810
811 #[allow(clippy::wrong_self_convention)]
814 fn run_end_from_arrow<R: RunEndIndexType>(
815 &self,
816 array: &dyn ArrowArray,
817 values_field: &Field,
818 ) -> VortexResult<ArrayRef>
819 where
820 R::Native: NativePType,
821 {
822 let run_array = array
823 .as_any()
824 .downcast_ref::<RunArray<R>>()
825 .ok_or_else(|| vortex_err!("expected an Arrow RunArray, got {}", array.data_type()))?;
826 let values =
827 self.from_arrow_array_inner(ArrowArrayRef::clone(run_array.values()), values_field)?;
828 run_end_from_arrow(run_array, values)
829 }
830}
831
832fn run_end_values_field(values_field: &FieldRef, nullability: Nullability) -> Field {
835 values_field
836 .as_ref()
837 .clone()
838 .with_nullable(nullability.into())
839}
840
841impl SessionVar for ArrowSession {
842 fn as_any(&self) -> &dyn Any {
843 self
844 }
845
846 fn as_any_mut(&mut self) -> &mut dyn Any {
847 self
848 }
849}
850
851#[derive(Clone, Copy, Debug)]
853pub struct ArrowExporter<'a> {
854 session: &'a ArrowSession,
855 options: &'a ArrowExportOptions,
856}
857
858impl ArrowExporter<'_> {
859 pub(crate) fn options(&self) -> &ArrowExportOptions {
860 self.options
861 }
862
863 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
868 pub fn execute_arrow(
869 &self,
870 array: ArrayRef,
871 target: Option<&Field>,
872 ctx: &mut ExecutionCtx,
873 ) -> VortexResult<ArrowArrayRef> {
874 let arrow_field;
878 let target_field = match target {
879 Some(field) => field,
880 None => {
881 arrow_field = self.session.to_arrow_field("", array.dtype())?;
882 &arrow_field
883 }
884 };
885
886 if let Some(arrow_ext_name) = target_field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
887 let len = array.len();
890 let mut current = array;
891
892 for plugin in self.session.exporters(&Id::new(arrow_ext_name)).iter() {
893 trace!(
894 plugin = ?plugin,
895 extension_name = arrow_ext_name,
896 "probing plugin for converting Arrow array"
897 );
898
899 match plugin.execute_arrow(current, target_field, self.options, ctx)? {
900 ArrowExport::Exported(arrow) => {
901 vortex_ensure_eq!(
902 arrow.len(),
903 len,
904 "Arrow array length does not match Vortex array length after conversion to {:?}",
905 arrow
906 );
907 return Ok(arrow);
908 }
909 ArrowExport::Unsupported(array) => current = array,
910 }
911 }
912
913 debug!(
914 extension_id = arrow_ext_name,
915 data_type = ?target_field.data_type(),
916 "unsupported Arrow extension type encountered, falling back to naive execution"
917 );
918
919 return execute_arrow_naive(current, Some(target_field.data_type()), self, ctx);
920 }
921
922 execute_arrow_naive(array, target.map(|field| field.data_type()), self, ctx)
923 }
924}
925
926pub trait ArrowSessionExt: SessionExt {
928 fn arrow(&self) -> SessionGuard<'_, ArrowSession>;
930}
931
932impl<S: SessionExt> ArrowSessionExt for S {
933 fn arrow(&self) -> SessionGuard<'_, ArrowSession> {
934 self.get::<ArrowSession>()
935 }
936}
937
938#[cfg(test)]
939mod tests {
940 use std::ops::Range;
941 use std::sync::Arc;
942
943 use arrow_array::DictionaryArray;
944 use arrow_array::GenericListArray;
945 use arrow_array::GenericListViewArray;
946 use arrow_array::Int32Array;
947 use arrow_array::Int64Array;
948 use arrow_array::MapArray;
949 use arrow_array::StringArray;
950 use arrow_array::StructArray;
951 use arrow_array::types::Int32Type;
952 use arrow_buffer::OffsetBuffer;
953 use arrow_buffer::ScalarBuffer;
954 use arrow_schema::DataType;
955 use arrow_schema::Field;
956 use arrow_schema::FieldRef;
957 use arrow_schema::Fields;
958 use rstest::rstest;
959 use vortex_array::IntoArray;
960 use vortex_array::VortexSessionExecute as _;
961 use vortex_array::array_session;
962 use vortex_array::arrays::Dict;
963 use vortex_array::arrays::ListArray;
964 use vortex_array::arrays::PrimitiveArray;
965 use vortex_array::arrays::VarBinViewArray;
966 use vortex_array::assert_arrays_eq;
967 use vortex_array::dtype::DType;
968 use vortex_array::dtype::Nullability;
969 use vortex_array::dtype::PType;
970 use vortex_array::validity::Validity;
971 use vortex_error::VortexResult;
972 use vortex_session::registry::CachedId;
973
974 use super::*;
975
976 #[rstest]
977 #[case::prefix(0, 2)]
978 #[case::middle(500, 2)]
979 #[case::empty(500, 0)]
980 #[case::empty_at_end(1000, 0)]
981 fn test_sliced_list_imports_only_its_rows(
982 #[values(false, true)] large: bool,
983 #[case] start: usize,
984 #[case] len: usize,
985 ) -> VortexResult<()> {
986 let lists = |rows: Range<i64>| -> ArrowArrayRef {
987 let values = Arc::new(Int64Array::from_iter_values(
988 rows.flat_map(|row| [row, row + 1]),
989 ));
990 let len = values.len() / 2;
991 let field = Arc::new(Field::new("item", DataType::Int64, false));
992 if large {
993 Arc::new(GenericListArray::<i64>::new(
994 field,
995 OffsetBuffer::from_repeated_length(2, len),
996 values,
997 None,
998 ))
999 } else {
1000 Arc::new(GenericListArray::<i32>::new(
1001 field,
1002 OffsetBuffer::from_repeated_length(2, len),
1003 values,
1004 None,
1005 ))
1006 }
1007 };
1008 let session = ArrowSession::default();
1009 let sliced = session.from_arrow_array(lists(0..1000).slice(start, len), false)?;
1010 let fresh = session.from_arrow_array(lists(start as i64..(start + len) as i64), false)?;
1011 assert_eq!(sliced.nbytes(), fresh.nbytes());
1012 let mut ctx = array_session().create_execution_ctx();
1013 let expected = ListArray::new(
1014 PrimitiveArray::from_iter(
1015 (start as i64..(start + len) as i64).flat_map(|row| [row, row + 1]),
1016 )
1017 .into_array(),
1018 PrimitiveArray::from_iter((0..=len as u64).map(|row| row * 2)).into_array(),
1019 Validity::NonNullable,
1020 &mut ctx,
1021 );
1022 assert_arrays_eq!(sliced, expected, &mut ctx);
1023 assert_arrays_eq!(fresh, expected, &mut ctx);
1024 Ok(())
1025 }
1026
1027 #[test]
1028 fn from_arrow_fields_matches_schema_conversion() -> VortexResult<()> {
1029 let session = ArrowSession::default();
1030 let fields = Fields::from(vec![
1031 Field::new("a", DataType::Int32, false),
1032 Field::new("b", DataType::Utf8View, true),
1033 ]);
1034 let struct_fields = session.from_arrow_fields(&fields)?;
1035 let schema_dtype = session.from_arrow_schema(&Schema::new(fields))?;
1036 let expected = StructFields::from_iter([
1037 ("a", DType::Primitive(PType::I32, Nullability::NonNullable)),
1038 ("b", DType::Utf8(Nullability::Nullable)),
1039 ]);
1040 assert_eq!(struct_fields, expected);
1041 assert_eq!(
1042 schema_dtype,
1043 DType::Struct(struct_fields, Nullability::NonNullable)
1044 );
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn from_arrow_field_maps_variant_without_importer() -> VortexResult<()> {
1050 let session = ArrowSession::default();
1051 let storage = DataType::Struct(
1052 vec![
1053 Field::new("metadata", DataType::BinaryView, false),
1054 Field::new("value", DataType::BinaryView, true),
1055 ]
1056 .into(),
1057 );
1058 let field = Field::new("v", storage, true).with_metadata(
1059 [(
1060 "ARROW:extension:name".to_string(),
1061 "arrow.parquet.variant".to_string(),
1062 )]
1063 .into(),
1064 );
1065 assert_eq!(
1066 session.from_arrow_field(&field)?,
1067 DType::Variant(Nullability::Nullable)
1068 );
1069 Ok(())
1070 }
1071
1072 #[test]
1075 fn dictionary_imports_as_dict_encoding() -> VortexResult<()> {
1076 let session = ArrowSession::default();
1077 let dict: ArrowArrayRef = Arc::new(DictionaryArray::<Int32Type>::try_new(
1078 Int32Array::from(vec![0, 1, 0, 1]),
1079 Arc::new(StringArray::from(vec!["a", "b"])),
1080 )?);
1081 let field = Field::new("s", dict.data_type().clone(), false);
1082
1083 let dtype = session.from_arrow_field(&field)?;
1084 assert_eq!(dtype, DType::Utf8(Nullability::NonNullable));
1085
1086 let array = session.from_arrow_array(dict, &field)?;
1087 assert!(array.is::<Dict>(), "expected a Dict encoding, got {array}");
1088 assert_eq!(array.dtype(), &dtype);
1089 assert_eq!(array.len(), 4);
1090 assert_arrays_eq!(
1091 array,
1092 VarBinViewArray::from_iter_str(["a", "b", "a", "b"]),
1093 &mut array_session().create_execution_ctx()
1094 );
1095 Ok(())
1096 }
1097
1098 static TRUNCATE_ID: CachedId = CachedId::new("test.truncate");
1099
1100 #[derive(Debug)]
1103 struct TruncatingImporter {
1104 decline: bool,
1105 }
1106
1107 impl ArrowImportVTable for TruncatingImporter {
1108 fn arrow_ext_id(&self) -> Id {
1109 *TRUNCATE_ID
1110 }
1111
1112 fn from_arrow_field(
1113 &self,
1114 field: &Field,
1115 session: &ArrowSession,
1116 ) -> VortexResult<Option<DType>> {
1117 session
1118 .from_arrow_datatype(field.data_type(), field.is_nullable().into())
1119 .map(Some)
1120 }
1121
1122 fn from_arrow_array(
1123 &self,
1124 array: ArrowArrayRef,
1125 field: &Field,
1126 _dtype: &DType,
1127 session: &ArrowSession,
1128 ) -> VortexResult<ArrowImport> {
1129 let truncated = array.slice(0, array.len() - 1);
1130 if self.decline {
1131 return Ok(ArrowImport::Unsupported(truncated));
1132 }
1133 session
1134 .from_arrow_array(truncated, field.is_nullable())
1135 .map(ArrowImport::Imported)
1136 }
1137 }
1138
1139 fn truncated_field(name: &str, data_type: DataType) -> FieldRef {
1140 Arc::new(
1141 Field::new(name, data_type, false).with_metadata(
1142 [(
1143 EXTENSION_TYPE_NAME_KEY.to_string(),
1144 "test.truncate".to_string(),
1145 )]
1146 .into(),
1147 ),
1148 )
1149 }
1150
1151 fn list_with_truncated_child() -> ArrowArrayRef {
1152 Arc::new(GenericListArray::<i32>::new(
1153 truncated_field("item", DataType::Int64),
1154 OffsetBuffer::new(vec![0, 5].into()),
1155 Arc::new(Int64Array::from_iter_values(0..5)),
1156 None,
1157 ))
1158 }
1159
1160 fn list_view_with_truncated_child() -> ArrowArrayRef {
1161 Arc::new(GenericListViewArray::<i32>::new(
1162 truncated_field("item", DataType::Int64),
1163 ScalarBuffer::from(vec![0]),
1164 ScalarBuffer::from(vec![5]),
1165 Arc::new(Int64Array::from_iter_values(0..5)),
1166 None,
1167 ))
1168 }
1169
1170 fn map_with_truncated_child() -> ArrowArrayRef {
1171 let key_field = Arc::new(Field::new("key", DataType::Int64, false));
1172 let value_field = Arc::new(Field::new("value", DataType::Int64, false));
1173 let entries = StructArray::new(
1174 Fields::from(vec![key_field, value_field]),
1175 vec![
1176 Arc::new(Int64Array::from_iter_values(0..5)),
1177 Arc::new(Int64Array::from_iter_values(5..10)),
1178 ],
1179 None,
1180 );
1181 Arc::new(MapArray::new(
1182 truncated_field("entries", entries.data_type().clone()),
1183 OffsetBuffer::new(vec![0, 5].into()),
1184 entries,
1185 None,
1186 false,
1187 ))
1188 }
1189
1190 #[rstest]
1191 #[case::list(list_with_truncated_child())]
1192 #[case::list_view(list_view_with_truncated_child())]
1193 #[case::map(map_with_truncated_child())]
1194 fn test_rejects_importer_that_changes_child_length(
1195 #[case] array: ArrowArrayRef,
1196 #[values(false, true)] decline: bool,
1197 ) {
1198 let session = ArrowSession::default();
1199 session.register_importer(Arc::new(TruncatingImporter { decline }));
1200
1201 let err = session
1202 .from_arrow_array(array, false)
1203 .expect_err("a child importer that drops elements must be rejected");
1204 assert!(
1205 err.to_string()
1206 .contains("returned 4 elements for an array of 5"),
1207 "unexpected error: {err}"
1208 );
1209 }
1210}