1use std::any::Any;
24use std::fmt::Debug;
25use std::sync::Arc;
26
27use arrow_array::Array as _;
28use arrow_array::ArrayRef as ArrowArrayRef;
29use arrow_array::RecordBatch;
30use arrow_array::make_array;
31use arrow_schema::DataType;
32use arrow_schema::Field;
33use arrow_schema::Fields;
34use arrow_schema::Schema;
35use arrow_schema::extension::EXTENSION_TYPE_NAME_KEY;
36use arrow_schema::extension::ExtensionType;
37use tracing::debug;
38use tracing::trace;
39use vortex_array::ArrayRef;
40use vortex_array::ExecutionCtx;
41use vortex_array::IntoArray;
42use vortex_array::arrays::FixedSizeListArray;
43use vortex_array::arrays::ListArray;
44use vortex_array::arrays::ListViewArray;
45use vortex_array::arrays::StructArray;
46use vortex_array::dtype::DType;
47use vortex_array::dtype::FieldName;
48use vortex_array::dtype::FieldNames;
49use vortex_array::dtype::Nullability;
50use vortex_array::dtype::StructFields;
51use vortex_array::dtype::extension::ExtId;
52use vortex_array::extension::datetime::AnyTemporal;
53use vortex_array::extension::uuid::Uuid;
54use vortex_array::validity::Validity;
55use vortex_error::VortexResult;
56use vortex_error::vortex_bail;
57use vortex_error::vortex_ensure;
58use vortex_session::ArcSwapMap;
59use vortex_session::SessionExt;
60use vortex_session::SessionGuard;
61use vortex_session::SessionVar;
62use vortex_session::registry::Id;
63
64use crate::FromArrowArray;
65use crate::IntoVortexArray;
66use crate::convert::map_from_arrow_parts;
67use crate::convert::nulls;
68use crate::convert::remove_nulls;
69use crate::dtype::TryFromArrowType;
70use crate::dtype::to_data_type_naive;
71use crate::executor::execute_arrow_naive;
72
73pub enum ArrowExport {
79 Unsupported(ArrayRef),
81 Exported(ArrowArrayRef),
83}
84
85pub enum ArrowImport {
91 Unsupported(ArrowArrayRef),
93 Imported(ArrayRef),
95}
96
97pub trait ArrowExportVTable: 'static + Send + Sync + Debug {
102 fn arrow_ext_id(&self) -> Id;
104
105 fn vortex_id(&self) -> Id;
109
110 fn to_arrow_field(
113 &self,
114 name: &str,
115 dtype: &DType,
116 session: &ArrowSession,
117 ) -> VortexResult<Option<Field>>;
118
119 fn execute_arrow(
124 &self,
125 array: ArrayRef,
126 target: &Field,
127 ctx: &mut ExecutionCtx,
128 ) -> VortexResult<ArrowExport>;
129}
130
131pub trait ArrowImportVTable: 'static + Send + Sync + Debug {
138 fn arrow_ext_id(&self) -> Id;
140
141 #[allow(clippy::wrong_self_convention)]
144 fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>>;
145
146 #[allow(clippy::wrong_self_convention)]
151 fn from_arrow_array(
152 &self,
153 array: ArrowArrayRef,
154 field: &Field,
155 dtype: &DType,
156 ) -> VortexResult<ArrowImport>;
157}
158
159pub type ArrowExportVTableRef = Arc<dyn ArrowExportVTable>;
160pub type ArrowImportVTableRef = Arc<dyn ArrowImportVTable>;
161
162type ArrowExporterRegistry = ArcSwapMap<Id, Arc<[ArrowExportVTableRef]>>;
164type VortexExporterRegistry = ArcSwapMap<ExtId, Arc<[ArrowExportVTableRef]>>;
166type ArrowImporterRegistry = ArcSwapMap<Id, Arc<[ArrowImportVTableRef]>>;
168
169#[derive(Clone, Debug)]
179pub struct ArrowSession {
180 exporters: ArrowExporterRegistry,
181 exporters_by_vortex: VortexExporterRegistry,
182 importers: ArrowImporterRegistry,
183}
184
185impl Default for ArrowSession {
186 fn default() -> Self {
187 let session = Self {
188 exporters: ArrowExporterRegistry::default(),
189 exporters_by_vortex: VortexExporterRegistry::default(),
190 importers: ArrowImporterRegistry::default(),
191 };
192
193 session.register_exporter(Arc::new(Uuid));
194 session.register_importer(Arc::new(Uuid));
195
196 session
197 }
198}
199
200impl ArrowSession {
201 pub fn register_exporter(&self, exporter: ArrowExportVTableRef) {
204 self.exporters.push(
205 exporter.arrow_ext_id(),
206 ArrowExportVTableRef::clone(&exporter),
207 );
208 self.exporters_by_vortex
209 .push(exporter.vortex_id(), exporter);
210 }
211
212 pub fn register_importer(&self, importer: ArrowImportVTableRef) {
214 self.importers.push(importer.arrow_ext_id(), importer);
215 }
216
217 fn exporters(&self, id: &Id) -> Arc<[ArrowExportVTableRef]> {
218 self.exporters.get(id).unwrap_or_else(|| Arc::from([]))
219 }
220
221 fn exporters_by_vortex(&self, id: &Id) -> Arc<[ArrowExportVTableRef]> {
222 self.exporters_by_vortex
223 .get(id)
224 .unwrap_or_else(|| Arc::from([]))
225 }
226
227 fn importers(&self, id: &Id) -> Arc<[ArrowImportVTableRef]> {
228 self.importers.get(id).unwrap_or_else(|| Arc::from([]))
229 }
230
231 pub fn to_arrow_field(&self, name: &str, dtype: &DType) -> VortexResult<Field> {
236 match dtype {
238 DType::List(elem_dtype, nullability) => {
239 let elem_field = self.to_arrow_field(Field::LIST_FIELD_DEFAULT_NAME, elem_dtype)?;
240 Ok(Field::new_list(name, elem_field, nullability.is_nullable()))
241 }
242 DType::FixedSizeList(elem_dtype, elem_size, nullability) => {
243 let elem_field = self.to_arrow_field(Field::LIST_FIELD_DEFAULT_NAME, elem_dtype)?;
244 Ok(Field::new_fixed_size_list(
245 name,
246 elem_field,
247 (*elem_size).try_into()?,
248 nullability.is_nullable(),
249 ))
250 }
251 DType::Map(map_dtype, nullability) => {
252 let key = self.to_arrow_field("key", &map_dtype.key_dtype())?;
253 let value = self.to_arrow_field("value", &map_dtype.value_dtype())?;
254 let entries = Field::new_struct("entries", Fields::from(vec![key, value]), false);
255 Ok(Field::new(
256 name,
257 DataType::Map(Arc::new(entries), map_dtype.keys_sorted()),
258 nullability.is_nullable(),
259 ))
260 }
261 DType::Struct(fields, nullability) => {
262 let arrow_fields = Fields::from_iter(
263 fields
264 .fields()
265 .zip(fields.names().iter())
266 .map(|(field, name)| self.to_arrow_field(name.as_ref(), &field))
267 .collect::<VortexResult<Vec<_>>>()?,
268 );
269 Ok(Field::new_struct(
270 name,
271 arrow_fields,
272 nullability.is_nullable(),
273 ))
274 }
275 DType::Extension(ext) if !ext.is::<AnyTemporal>() => {
276 for plugin in self.exporters_by_vortex(&ext.id()).iter() {
277 if let Some(field) =
278 plugin.to_arrow_field(name, &DType::Extension(ext.clone()), self)?
279 {
280 return Ok(field);
281 }
282 }
283 vortex_bail!("extension type cannot be converted to Arrow without a plugin: {ext}");
284 }
285 DType::Variant(_) => {
286 Ok(Field::new(
290 name,
291 DataType::Struct(
292 vec![
293 Field::new("metadata", DataType::BinaryView, dtype.is_nullable()),
294 Field::new("value", DataType::BinaryView, dtype.is_nullable()),
295 ]
296 .into(),
297 ),
298 dtype.is_nullable(),
299 )
300 .with_metadata(
301 [(
302 EXTENSION_TYPE_NAME_KEY.to_string(),
303 "arrow.parquet.variant".to_string(),
304 )]
305 .into(),
306 ))
307 }
308 _ => Ok(Field::new(
309 name,
310 to_data_type_naive(dtype)?,
311 dtype.is_nullable(),
312 )),
313 }
314 }
315
316 pub fn to_arrow_schema(&self, dtype: &DType) -> VortexResult<Schema> {
320 let DType::Struct(struct_dtype, _) = dtype else {
321 vortex_bail!("to_arrow_schema requires a top-level struct dtype, got {dtype}");
322 };
323 let mut fields = Vec::with_capacity(struct_dtype.names().len());
324 for (name, field_dtype) in struct_dtype.names().iter().zip(struct_dtype.fields()) {
325 fields.push(self.to_arrow_field(name.as_ref(), &field_dtype)?);
326 }
327 Ok(Schema::new(fields))
328 }
329
330 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
339 pub fn from_arrow_field(&self, field: &Field) -> VortexResult<DType> {
340 if let Some(name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
341 for plugin in self.importers(&Id::new(name)).iter() {
342 if let Some(dtype) = plugin.from_arrow_field(field)? {
343 return Ok(dtype);
344 }
345 }
346 }
347 let nullability: Nullability = field.is_nullable().into();
348 Ok(match field.data_type() {
349 DataType::List(elem)
350 | DataType::LargeList(elem)
351 | DataType::ListView(elem)
352 | DataType::LargeListView(elem) => {
353 DType::List(Arc::new(self.from_arrow_field(elem.as_ref())?), nullability)
354 }
355 DataType::FixedSizeList(elem, size) => DType::FixedSizeList(
356 Arc::new(self.from_arrow_field(elem.as_ref())?),
357 *size as u32,
358 nullability,
359 ),
360 DataType::Map(entries, keys_sorted) => {
361 vortex_ensure!(
362 !entries.is_nullable(),
363 "Arrow map entries field must be non-nullable"
364 );
365 let DataType::Struct(fields) = entries.data_type() else {
366 vortex_bail!(
367 "Arrow map entries field must have Struct type, got {:?}",
368 entries.data_type()
369 );
370 };
371 vortex_ensure!(
372 fields.len() == 2,
373 "Arrow map entries struct must contain exactly two fields"
374 );
375 vortex_ensure!(
376 !fields[0].is_nullable(),
377 "Arrow map key field must be non-nullable"
378 );
379 DType::map(
380 self.from_arrow_field(fields[0].as_ref())?,
381 self.from_arrow_field(fields[1].as_ref())?,
382 *keys_sorted,
383 nullability,
384 )?
385 }
386 DataType::Struct(fields) => {
387 let entries = fields
388 .iter()
389 .map(|f| {
390 self.from_arrow_field(f)
391 .map(|dt| (FieldName::from(f.name().as_str()), dt))
392 })
393 .collect::<VortexResult<Vec<_>>>()?;
394 DType::Struct(StructFields::from_iter(entries), nullability)
395 }
396 _ => DType::try_from_arrow(field)?,
397 })
398 }
399
400 pub fn from_arrow_schema(&self, schema: &Schema) -> VortexResult<DType> {
404 let entries = schema
405 .fields()
406 .iter()
407 .map(|f| {
408 self.from_arrow_field(f)
409 .map(|dt| (FieldName::from(f.name().as_str()), dt))
410 })
411 .collect::<VortexResult<Vec<_>>>()?;
412 Ok(DType::Struct(
413 StructFields::from_iter(entries),
414 Nullability::NonNullable,
415 ))
416 }
417
418 pub fn from_arrow_record_batch(
426 &self,
427 batch: RecordBatch,
428 schema: &Schema,
429 ) -> VortexResult<ArrayRef> {
430 vortex_ensure!(
431 batch.num_columns() == schema.fields().len(),
432 "RecordBatch has {} columns but schema has {} fields",
433 batch.num_columns(),
434 schema.fields().len()
435 );
436 let length = batch.num_rows();
437 let names = FieldNames::from_iter(
438 schema
439 .fields()
440 .iter()
441 .map(|f| FieldName::from(f.name().as_str())),
442 );
443 let mut columns = Vec::with_capacity(schema.fields().len());
444 for (col, field) in batch.columns().iter().zip(schema.fields().iter()) {
445 columns.push(self.from_arrow_array(ArrowArrayRef::clone(col), field)?);
446 }
447 Ok(StructArray::try_new(names, columns, length, Validity::NonNullable)?.into_array())
448 }
449
450 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
458 pub fn execute_arrow(
459 &self,
460 array: ArrayRef,
461 target: Option<&Field>,
462 ctx: &mut ExecutionCtx,
463 ) -> VortexResult<ArrowArrayRef> {
464 let arrow_field;
468 let target_field = match target {
469 Some(field) => field,
470 None => {
471 let session = ctx.session().clone();
472 arrow_field = session.arrow().to_arrow_field("", array.dtype())?;
473 &arrow_field
474 }
475 };
476
477 if let Some(arrow_ext_name) = target_field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
478 let len = array.len();
481 let mut current = array;
482
483 for plugin in self.exporters(&Id::new(arrow_ext_name)).iter() {
484 trace!(
485 plugin = ?plugin,
486 extension_name = arrow_ext_name,
487 "probing plugin for converting Arrow array"
488 );
489
490 match plugin.execute_arrow(current, target_field, ctx)? {
491 ArrowExport::Exported(arrow) => {
492 vortex_ensure!(
493 arrow.len() == len,
494 "Arrow array length does not match Vortex array length after conversion to {:?}",
495 arrow
496 );
497 return Ok(arrow);
498 }
499 ArrowExport::Unsupported(array) => current = array,
500 }
501 }
502
503 debug!(
504 extension_id = arrow_ext_name,
505 data_type = ?target_field.data_type(),
506 "unsupported Arrow extension type encountered, falling back to naive execution"
507 );
508
509 return execute_arrow_naive(current, Some(target_field.data_type()), ctx);
510 }
511
512 execute_arrow_naive(array, target.map(|field| field.data_type()), ctx)
513 }
514
515 pub fn from_arrow_array(&self, array: ArrowArrayRef, field: &Field) -> VortexResult<ArrayRef> {
525 if let Some(extension_name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
526 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
527 let importers = self.importers(&Id::new(extension_name));
528 if !importers.is_empty() {
529 let dtype = self.from_arrow_field(field)?;
530 let mut current = array;
531 for plugin in importers.iter() {
532 match plugin.from_arrow_array(current, field, &dtype)? {
533 ArrowImport::Imported(arr) => return Ok(arr),
534 ArrowImport::Unsupported(arr) => current = arr,
535 }
536 }
537 return ArrayRef::from_arrow(current.as_ref(), field.is_nullable());
538 }
539 }
540 self.from_arrow_array_canonical(array, field)
541 }
542
543 #[allow(clippy::wrong_self_convention)]
546 fn from_arrow_array_canonical(
547 &self,
548 array: ArrowArrayRef,
549 field: &Field,
550 ) -> VortexResult<ArrayRef> {
551 use arrow_array::cast::AsArray;
552
553 match field.data_type() {
554 DataType::Struct(fields) => {
555 let arrow_struct = array.as_struct();
556 let names = FieldNames::from_iter(
557 fields.iter().map(|f| FieldName::from(f.name().as_str())),
558 );
559 let columns = arrow_struct
560 .columns()
561 .iter()
562 .zip(fields.iter())
563 .map(|(col, child_field)| {
564 let inner = if col.null_count() > 0 && !child_field.is_nullable() {
567 make_array(remove_nulls(col.to_data())?)
568 } else {
569 ArrowArrayRef::clone(col)
570 };
571 self.from_arrow_array(inner, child_field.as_ref())
572 })
573 .collect::<VortexResult<Vec<_>>>()?;
574 let validity = nulls(arrow_struct.nulls(), field.is_nullable())?;
575 Ok(
576 StructArray::try_new(names, columns, arrow_struct.len(), validity)?
577 .into_array(),
578 )
579 }
580 DataType::List(elem_field) => {
581 let list = array.as_list::<i32>();
582 let elements = self
583 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
584 let offsets = list.offsets().clone().into_array();
585 let validity = nulls(list.nulls(), field.is_nullable())?;
586 Ok(ListArray::try_new(elements, offsets, validity)?.into_array())
587 }
588 DataType::LargeList(elem_field) => {
589 let list = array.as_list::<i64>();
590 let elements = self
591 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
592 let offsets = list.offsets().clone().into_array();
593 let validity = nulls(list.nulls(), field.is_nullable())?;
594 Ok(ListArray::try_new(elements, offsets, validity)?.into_array())
595 }
596 DataType::FixedSizeList(elem_field, list_size) => {
597 let fsl = array.as_fixed_size_list();
598 let elements =
599 self.from_arrow_array(ArrowArrayRef::clone(fsl.values()), elem_field.as_ref())?;
600 let validity = nulls(fsl.nulls(), field.is_nullable())?;
601 Ok(
602 FixedSizeListArray::try_new(elements, *list_size as u32, validity, fsl.len())?
603 .into_array(),
604 )
605 }
606 DataType::ListView(elem_field) => {
607 let list = array.as_list_view::<i32>();
608 let elements = self
609 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
610 let offsets = list.offsets().clone().into_array();
611 let sizes = list.sizes().clone().into_array();
612 let validity = nulls(list.nulls(), field.is_nullable())?;
613 Ok(ListViewArray::try_new(elements, offsets, sizes, validity)?.into_array())
614 }
615 DataType::LargeListView(elem_field) => {
616 let list = array.as_list_view::<i64>();
617 let elements = self
618 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
619 let offsets = list.offsets().clone().into_array();
620 let sizes = list.sizes().clone().into_array();
621 let validity = nulls(list.nulls(), field.is_nullable())?;
622 Ok(ListViewArray::try_new(elements, offsets, sizes, validity)?.into_array())
623 }
624 DataType::Map(entries_field, keys_sorted) => {
625 let map = array.as_map();
626 let entries_array: ArrowArrayRef = Arc::new(map.entries().clone());
627 let entries = self.from_arrow_array(entries_array, entries_field.as_ref())?;
628 map_from_arrow_parts(
629 entries,
630 map.offsets(),
631 map.nulls(),
632 *keys_sorted,
633 field.is_nullable(),
634 )
635 }
636 _ => ArrayRef::from_arrow(array.as_ref(), field.is_nullable()),
637 }
638 }
639}
640
641pub(crate) fn has_valid_extension_type<E: ExtensionType>(field: &Field) -> bool {
645 if field.extension_type_name() != Some(E::NAME) {
646 return false;
647 }
648
649 E::try_new_from_field_metadata(field.data_type(), field.metadata()).is_ok()
650}
651
652impl SessionVar for ArrowSession {
653 fn as_any(&self) -> &dyn Any {
654 self
655 }
656
657 fn as_any_mut(&mut self) -> &mut dyn Any {
658 self
659 }
660}
661
662pub trait ArrowSessionExt: SessionExt {
664 fn arrow(&self) -> SessionGuard<'_, ArrowSession>;
666}
667
668impl<S: SessionExt> ArrowSessionExt for S {
669 fn arrow(&self) -> SessionGuard<'_, ArrowSession> {
670 self.get::<ArrowSession>()
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use std::sync::Arc;
677
678 use arrow_array::FixedSizeBinaryArray;
679 use arrow_array::cast::AsArray;
680 use arrow_schema::DataType;
681 use arrow_schema::Field;
682 use arrow_schema::extension::Uuid as ArrowUuid;
683 use vortex_array::VortexSessionExecute;
684 use vortex_array::array_session;
685 use vortex_array::dtype::DType;
686 use vortex_array::dtype::FieldName;
687 use vortex_array::dtype::Nullability;
688 use vortex_array::dtype::PType;
689 use vortex_array::dtype::StructFields;
690 use vortex_array::dtype::extension::ExtDType;
691 use vortex_array::dtype::extension::ExtVTable;
692 use vortex_array::extension::uuid::Uuid;
693 use vortex_array::extension::uuid::UuidMetadata;
694 use vortex_error::VortexResult;
695
696 use super::*;
697
698 fn uuid_dtype(nullable: bool) -> DType {
699 let storage = DType::FixedSizeList(
700 Arc::new(DType::Primitive(PType::U8, Nullability::NonNullable)),
701 16,
702 nullable.into(),
703 );
704 DType::Extension(
705 ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage)
706 .expect("uuid ext dtype")
707 .erased(),
708 )
709 }
710
711 #[test]
712 fn to_arrow_field_top_level_uuid_carries_extension_metadata() -> VortexResult<()> {
713 let session = ArrowSession::default();
714 let field = session.to_arrow_field("id", &uuid_dtype(false))?;
715 assert!(has_valid_extension_type::<ArrowUuid>(&field));
716 Ok(())
717 }
718
719 #[test]
720 fn to_arrow_field_struct_with_nested_uuid_preserves_metadata() -> VortexResult<()> {
721 let session = ArrowSession::default();
722 let dtype = DType::Struct(
723 StructFields::from_iter([(FieldName::from("id"), uuid_dtype(false))]),
724 Nullability::NonNullable,
725 );
726 let field = session.to_arrow_field("row", &dtype)?;
727 let DataType::Struct(inner) = field.data_type() else {
728 panic!("expected Struct, got {:?}", field.data_type());
729 };
730 assert_eq!(inner.len(), 1);
731 assert_eq!(inner[0].data_type(), &DataType::FixedSizeBinary(16));
732 assert!(has_valid_extension_type::<ArrowUuid>(&inner[0]));
733 Ok(())
734 }
735
736 #[test]
737 fn to_arrow_field_list_of_uuid_preserves_metadata() -> VortexResult<()> {
738 let session = ArrowSession::default();
739 let dtype = DType::List(Arc::new(uuid_dtype(true)), Nullability::NonNullable);
740 let field = session.to_arrow_field("ids", &dtype)?;
741 let DataType::List(elem) = field.data_type() else {
742 panic!("expected List, got {:?}", field.data_type());
743 };
744 assert!(has_valid_extension_type::<ArrowUuid>(elem));
745 Ok(())
746 }
747
748 #[test]
749 fn to_arrow_field_fixed_size_list_of_uuid_preserves_metadata() -> VortexResult<()> {
750 let session = ArrowSession::default();
751 let dtype = DType::FixedSizeList(Arc::new(uuid_dtype(false)), 3, Nullability::NonNullable);
752 let field = session.to_arrow_field("triple", &dtype)?;
753 let DataType::FixedSizeList(elem, size) = field.data_type() else {
754 panic!("expected FixedSizeList, got {:?}", field.data_type());
755 };
756 assert_eq!(*size, 3);
757 assert!(has_valid_extension_type::<ArrowUuid>(elem));
758 Ok(())
759 }
760
761 #[test]
762 fn schema_roundtrip_preserves_map_uuid_fields() -> VortexResult<()> {
763 let session = ArrowSession::default();
764 let map = DType::map(
765 uuid_dtype(false),
766 uuid_dtype(true),
767 true,
768 Nullability::Nullable,
769 )?;
770 let dtype = DType::Struct(
771 StructFields::from_iter([(FieldName::from("ids"), map)]),
772 Nullability::NonNullable,
773 );
774
775 let schema = session.to_arrow_schema(&dtype)?;
776 let field = schema.field(0);
777 let DataType::Map(entries, keys_sorted) = field.data_type() else {
778 panic!("expected Map, got {:?}", field.data_type());
779 };
780 assert!(*keys_sorted);
781 assert_eq!(entries.name(), "entries");
782 assert!(!entries.is_nullable());
783 let DataType::Struct(fields) = entries.data_type() else {
784 panic!("expected map entries struct, got {:?}", entries.data_type());
785 };
786 assert!(has_valid_extension_type::<ArrowUuid>(&fields[0]));
787 assert!(has_valid_extension_type::<ArrowUuid>(&fields[1]));
788 assert!(!fields[0].is_nullable());
789 assert!(fields[1].is_nullable());
790
791 assert_eq!(session.from_arrow_schema(&schema)?, dtype);
792 Ok(())
793 }
794
795 #[test]
796 fn to_arrow_schema_struct_of_struct_uuid() -> VortexResult<()> {
797 let session = ArrowSession::default();
798 let inner = DType::Struct(
799 StructFields::from_iter([(FieldName::from("id"), uuid_dtype(true))]),
800 Nullability::NonNullable,
801 );
802 let outer = DType::Struct(
803 StructFields::from_iter([(FieldName::from("payload"), inner)]),
804 Nullability::NonNullable,
805 );
806 let schema = session.to_arrow_schema(&outer)?;
807 let payload = schema.field(0);
808 let DataType::Struct(inner_fields) = payload.data_type() else {
809 panic!("expected Struct, got {:?}", payload.data_type());
810 };
811 assert!(has_valid_extension_type::<ArrowUuid>(&inner_fields[0]));
812 Ok(())
813 }
814
815 #[test]
816 fn from_arrow_field_recurses_into_nested_uuid() -> VortexResult<()> {
817 let session = ArrowSession::default();
818 let mut elem = Field::new("item", DataType::FixedSizeBinary(16), false);
819 elem.try_with_extension_type(ArrowUuid)?;
820 let outer = Field::new("ids", DataType::List(Arc::new(elem)), false);
821
822 let dtype = session.from_arrow_field(&outer)?;
823 let DType::List(inner_dt, _) = dtype else {
824 panic!("expected List dtype, got {dtype}");
825 };
826 assert!(
827 matches!(inner_dt.as_ref(), DType::Extension(ext) if ext.id() == Uuid.id()),
828 "expected Uuid extension element, got {inner_dt}",
829 );
830 Ok(())
831 }
832
833 #[test]
834 fn schema_roundtrip_preserves_nested_uuid() -> VortexResult<()> {
835 let session = ArrowSession::default();
836 let dtype = DType::Struct(
837 StructFields::from_iter([
838 (FieldName::from("id"), uuid_dtype(false)),
839 (
840 FieldName::from("ids"),
841 DType::List(Arc::new(uuid_dtype(true)), Nullability::NonNullable),
842 ),
843 ]),
844 Nullability::NonNullable,
845 );
846 let schema = session.to_arrow_schema(&dtype)?;
847 let roundtripped = session.from_arrow_schema(&schema)?;
848 assert_eq!(roundtripped, dtype);
849 Ok(())
850 }
851
852 #[test]
853 fn execute_arrow_target_none_preserves_top_level_uuid_metadata() -> VortexResult<()> {
854 let vortex_session = array_session();
855 let mut ctx = vortex_session.create_execution_ctx();
856 let session = vortex_session.arrow();
857
858 let mut field = Field::new("id", DataType::FixedSizeBinary(16), false);
859 field.try_with_extension_type(ArrowUuid)?;
860 let arrow_array: ArrowArrayRef = Arc::new(FixedSizeBinaryArray::try_from_iter(
861 [*b"0123456789abcdef", *b"fedcba9876543210"].into_iter(),
862 )?);
863
864 let vortex_array = session.from_arrow_array(arrow_array, &field)?;
865
866 let vortex_ext = vortex_array.dtype().as_extension();
867 assert!(vortex_ext.is::<Uuid>());
868
869 let exported = session.execute_arrow(vortex_array, None, &mut ctx)?;
870 assert_eq!(exported.data_type(), &DataType::FixedSizeBinary(16));
871 let fsb = exported.as_fixed_size_binary();
872 assert_eq!(fsb.len(), 2);
873 assert_eq!(fsb.value(0), b"0123456789abcdef");
874 assert_eq!(fsb.value(1), b"fedcba9876543210");
875 Ok(())
876 }
877}