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::arc_swap_map::ArcSwapMap;
43use vortex_array::arrays::FixedSizeListArray;
44use vortex_array::arrays::ListArray;
45use vortex_array::arrays::ListViewArray;
46use vortex_array::arrays::StructArray;
47use vortex_array::dtype::DType;
48use vortex_array::dtype::FieldName;
49use vortex_array::dtype::FieldNames;
50use vortex_array::dtype::Nullability;
51use vortex_array::dtype::StructFields;
52use vortex_array::dtype::extension::ExtId;
53use vortex_array::extension::datetime::AnyTemporal;
54use vortex_array::extension::uuid::Uuid;
55use vortex_array::validity::Validity;
56use vortex_error::VortexResult;
57use vortex_error::vortex_bail;
58use vortex_error::vortex_ensure;
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::nulls;
67use crate::convert::remove_nulls;
68use crate::dtype::TryFromArrowType;
69use crate::dtype::to_data_type_naive;
70use crate::executor::execute_arrow_naive;
71
72pub enum ArrowExport {
78 Unsupported(ArrayRef),
80 Exported(ArrowArrayRef),
82}
83
84pub enum ArrowImport {
90 Unsupported(ArrowArrayRef),
92 Imported(ArrayRef),
94}
95
96pub trait ArrowExportVTable: 'static + Send + Sync + Debug {
101 fn arrow_ext_id(&self) -> Id;
103
104 fn vortex_id(&self) -> Id;
108
109 fn to_arrow_field(
112 &self,
113 name: &str,
114 dtype: &DType,
115 session: &ArrowSession,
116 ) -> VortexResult<Option<Field>>;
117
118 fn execute_arrow(
123 &self,
124 array: ArrayRef,
125 target: &Field,
126 ctx: &mut ExecutionCtx,
127 ) -> VortexResult<ArrowExport>;
128}
129
130pub trait ArrowImportVTable: 'static + Send + Sync + Debug {
137 fn arrow_ext_id(&self) -> Id;
139
140 #[allow(clippy::wrong_self_convention)]
143 fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>>;
144
145 #[allow(clippy::wrong_self_convention)]
150 fn from_arrow_array(
151 &self,
152 array: ArrowArrayRef,
153 field: &Field,
154 dtype: &DType,
155 ) -> VortexResult<ArrowImport>;
156}
157
158pub type ArrowExportVTableRef = Arc<dyn ArrowExportVTable>;
159pub type ArrowImportVTableRef = Arc<dyn ArrowImportVTable>;
160
161#[derive(Clone, Debug)]
171pub struct ArrowSession {
172 exporters: ArcSwapMap<Id, Arc<[ArrowExportVTableRef]>>,
173 exporters_by_vortex: ArcSwapMap<ExtId, Arc<[ArrowExportVTableRef]>>,
174 importers: ArcSwapMap<Id, Arc<[ArrowImportVTableRef]>>,
175}
176
177impl Default for ArrowSession {
178 fn default() -> Self {
179 let session = Self {
180 exporters: ArcSwapMap::default(),
181 exporters_by_vortex: ArcSwapMap::default(),
182 importers: ArcSwapMap::default(),
183 };
184
185 session.register_exporter(Arc::new(Uuid));
186 session.register_importer(Arc::new(Uuid));
187
188 session
189 }
190}
191
192impl ArrowSession {
193 pub fn register_exporter(&self, exporter: ArrowExportVTableRef) {
196 self.exporters.push(
197 exporter.arrow_ext_id(),
198 ArrowExportVTableRef::clone(&exporter),
199 );
200 self.exporters_by_vortex
201 .push(exporter.vortex_id(), exporter);
202 }
203
204 pub fn register_importer(&self, importer: ArrowImportVTableRef) {
206 self.importers.push(importer.arrow_ext_id(), importer);
207 }
208
209 fn exporters(&self, id: &Id) -> Arc<[ArrowExportVTableRef]> {
210 self.exporters.get(id).unwrap_or_else(|| Arc::from([]))
211 }
212
213 fn exporters_by_vortex(&self, id: &Id) -> Arc<[ArrowExportVTableRef]> {
214 self.exporters_by_vortex
215 .get(id)
216 .unwrap_or_else(|| Arc::from([]))
217 }
218
219 fn importers(&self, id: &Id) -> Arc<[ArrowImportVTableRef]> {
220 self.importers.get(id).unwrap_or_else(|| Arc::from([]))
221 }
222
223 pub fn to_arrow_field(&self, name: &str, dtype: &DType) -> VortexResult<Field> {
228 match dtype {
230 DType::List(elem_dtype, nullability) => {
231 let elem_field = self.to_arrow_field(Field::LIST_FIELD_DEFAULT_NAME, elem_dtype)?;
232 Ok(Field::new_list(name, elem_field, nullability.is_nullable()))
233 }
234 DType::FixedSizeList(elem_dtype, elem_size, nullability) => {
235 let elem_field = self.to_arrow_field(Field::LIST_FIELD_DEFAULT_NAME, elem_dtype)?;
236 Ok(Field::new_fixed_size_list(
237 name,
238 elem_field,
239 (*elem_size).try_into()?,
240 nullability.is_nullable(),
241 ))
242 }
243 DType::Struct(fields, nullability) => {
244 let arrow_fields = Fields::from_iter(
245 fields
246 .fields()
247 .zip(fields.names().iter())
248 .map(|(field, name)| self.to_arrow_field(name.as_ref(), &field))
249 .collect::<VortexResult<Vec<_>>>()?,
250 );
251 Ok(Field::new_struct(
252 name,
253 arrow_fields,
254 nullability.is_nullable(),
255 ))
256 }
257 DType::Extension(ext) if !ext.is::<AnyTemporal>() => {
258 for plugin in self.exporters_by_vortex(&ext.id()).iter() {
259 if let Some(field) =
260 plugin.to_arrow_field(name, &DType::Extension(ext.clone()), self)?
261 {
262 return Ok(field);
263 }
264 }
265 vortex_bail!("extension type cannot be converted to Arrow without a plugin: {ext}");
266 }
267 DType::Variant(_) => {
268 Ok(Field::new(
272 name,
273 DataType::Struct(
274 vec![
275 Field::new("metadata", DataType::BinaryView, dtype.is_nullable()),
276 Field::new("value", DataType::BinaryView, dtype.is_nullable()),
277 ]
278 .into(),
279 ),
280 dtype.is_nullable(),
281 )
282 .with_metadata(
283 [(
284 EXTENSION_TYPE_NAME_KEY.to_string(),
285 "arrow.parquet.variant".to_string(),
286 )]
287 .into(),
288 ))
289 }
290 _ => Ok(Field::new(
291 name,
292 to_data_type_naive(dtype)?,
293 dtype.is_nullable(),
294 )),
295 }
296 }
297
298 pub fn to_arrow_schema(&self, dtype: &DType) -> VortexResult<Schema> {
302 let DType::Struct(struct_dtype, _) = dtype else {
303 vortex_bail!("to_arrow_schema requires a top-level struct dtype, got {dtype}");
304 };
305 let mut fields = Vec::with_capacity(struct_dtype.names().len());
306 for (name, field_dtype) in struct_dtype.names().iter().zip(struct_dtype.fields()) {
307 fields.push(self.to_arrow_field(name.as_ref(), &field_dtype)?);
308 }
309 Ok(Schema::new(fields))
310 }
311
312 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
321 pub fn from_arrow_field(&self, field: &Field) -> VortexResult<DType> {
322 if let Some(name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
323 for plugin in self.importers(&Id::new(name)).iter() {
324 if let Some(dtype) = plugin.from_arrow_field(field)? {
325 return Ok(dtype);
326 }
327 }
328 }
329 let nullability: Nullability = field.is_nullable().into();
330 Ok(match field.data_type() {
331 DataType::List(elem)
332 | DataType::LargeList(elem)
333 | DataType::ListView(elem)
334 | DataType::LargeListView(elem) => {
335 DType::List(Arc::new(self.from_arrow_field(elem.as_ref())?), nullability)
336 }
337 DataType::FixedSizeList(elem, size) => DType::FixedSizeList(
338 Arc::new(self.from_arrow_field(elem.as_ref())?),
339 *size as u32,
340 nullability,
341 ),
342 DataType::Struct(fields) => {
343 let entries = fields
344 .iter()
345 .map(|f| {
346 self.from_arrow_field(f)
347 .map(|dt| (FieldName::from(f.name().as_str()), dt))
348 })
349 .collect::<VortexResult<Vec<_>>>()?;
350 DType::Struct(StructFields::from_iter(entries), nullability)
351 }
352 _ => DType::try_from_arrow(field)?,
353 })
354 }
355
356 pub fn from_arrow_schema(&self, schema: &Schema) -> VortexResult<DType> {
360 let entries = schema
361 .fields()
362 .iter()
363 .map(|f| {
364 self.from_arrow_field(f)
365 .map(|dt| (FieldName::from(f.name().as_str()), dt))
366 })
367 .collect::<VortexResult<Vec<_>>>()?;
368 Ok(DType::Struct(
369 StructFields::from_iter(entries),
370 Nullability::NonNullable,
371 ))
372 }
373
374 pub fn from_arrow_record_batch(
382 &self,
383 batch: RecordBatch,
384 schema: &Schema,
385 ) -> VortexResult<ArrayRef> {
386 vortex_ensure!(
387 batch.num_columns() == schema.fields().len(),
388 "RecordBatch has {} columns but schema has {} fields",
389 batch.num_columns(),
390 schema.fields().len()
391 );
392 let length = batch.num_rows();
393 let names = FieldNames::from_iter(
394 schema
395 .fields()
396 .iter()
397 .map(|f| FieldName::from(f.name().as_str())),
398 );
399 let mut columns = Vec::with_capacity(schema.fields().len());
400 for (col, field) in batch.columns().iter().zip(schema.fields().iter()) {
401 columns.push(self.from_arrow_array(ArrowArrayRef::clone(col), field)?);
402 }
403 Ok(StructArray::try_new(names, columns, length, Validity::NonNullable)?.into_array())
404 }
405
406 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
414 pub fn execute_arrow(
415 &self,
416 array: ArrayRef,
417 target: Option<&Field>,
418 ctx: &mut ExecutionCtx,
419 ) -> VortexResult<ArrowArrayRef> {
420 let arrow_field;
424 let target_field = match target {
425 Some(field) => field,
426 None => {
427 let session = ctx.session().clone();
428 arrow_field = session.arrow().to_arrow_field("", array.dtype())?;
429 &arrow_field
430 }
431 };
432
433 if let Some(arrow_ext_name) = target_field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
434 let len = array.len();
437 let mut current = array;
438
439 for plugin in self.exporters(&Id::new(arrow_ext_name)).iter() {
440 trace!(
441 plugin = ?plugin,
442 extension_name = arrow_ext_name,
443 "probing plugin for converting Arrow array"
444 );
445
446 match plugin.execute_arrow(current, target_field, ctx)? {
447 ArrowExport::Exported(arrow) => {
448 vortex_ensure!(
449 arrow.len() == len,
450 "Arrow array length does not match Vortex array length after conversion to {:?}",
451 arrow
452 );
453 return Ok(arrow);
454 }
455 ArrowExport::Unsupported(array) => current = array,
456 }
457 }
458
459 debug!(
460 extension_id = arrow_ext_name,
461 data_type = ?target_field.data_type(),
462 "unsupported Arrow extension type encountered, falling back to naive execution"
463 );
464
465 return execute_arrow_naive(current, Some(target_field.data_type()), ctx);
466 }
467
468 execute_arrow_naive(array, target.map(|field| field.data_type()), ctx)
469 }
470
471 pub fn from_arrow_array(&self, array: ArrowArrayRef, field: &Field) -> VortexResult<ArrayRef> {
481 if let Some(extension_name) = field.metadata().get(EXTENSION_TYPE_NAME_KEY) {
482 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
483 let importers = self.importers(&Id::new(extension_name));
484 if !importers.is_empty() {
485 let dtype = self.from_arrow_field(field)?;
486 let mut current = array;
487 for plugin in importers.iter() {
488 match plugin.from_arrow_array(current, field, &dtype)? {
489 ArrowImport::Imported(arr) => return Ok(arr),
490 ArrowImport::Unsupported(arr) => current = arr,
491 }
492 }
493 return ArrayRef::from_arrow(current.as_ref(), field.is_nullable());
494 }
495 }
496 self.from_arrow_array_canonical(array, field)
497 }
498
499 #[allow(clippy::wrong_self_convention)]
502 fn from_arrow_array_canonical(
503 &self,
504 array: ArrowArrayRef,
505 field: &Field,
506 ) -> VortexResult<ArrayRef> {
507 use arrow_array::cast::AsArray;
508
509 match field.data_type() {
510 DataType::Struct(fields) => {
511 let arrow_struct = array.as_struct();
512 let names = FieldNames::from_iter(
513 fields.iter().map(|f| FieldName::from(f.name().as_str())),
514 );
515 let columns = arrow_struct
516 .columns()
517 .iter()
518 .zip(fields.iter())
519 .map(|(col, child_field)| {
520 let inner = if col.null_count() > 0 && !child_field.is_nullable() {
523 make_array(remove_nulls(col.to_data())?)
524 } else {
525 ArrowArrayRef::clone(col)
526 };
527 self.from_arrow_array(inner, child_field.as_ref())
528 })
529 .collect::<VortexResult<Vec<_>>>()?;
530 let validity = nulls(arrow_struct.nulls(), field.is_nullable())?;
531 Ok(
532 StructArray::try_new(names, columns, arrow_struct.len(), validity)?
533 .into_array(),
534 )
535 }
536 DataType::List(elem_field) => {
537 let list = array.as_list::<i32>();
538 let elements = self
539 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
540 let offsets = list.offsets().clone().into_array();
541 let validity = nulls(list.nulls(), field.is_nullable())?;
542 Ok(ListArray::try_new(elements, offsets, validity)?.into_array())
543 }
544 DataType::LargeList(elem_field) => {
545 let list = array.as_list::<i64>();
546 let elements = self
547 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
548 let offsets = list.offsets().clone().into_array();
549 let validity = nulls(list.nulls(), field.is_nullable())?;
550 Ok(ListArray::try_new(elements, offsets, validity)?.into_array())
551 }
552 DataType::FixedSizeList(elem_field, list_size) => {
553 let fsl = array.as_fixed_size_list();
554 let elements =
555 self.from_arrow_array(ArrowArrayRef::clone(fsl.values()), elem_field.as_ref())?;
556 let validity = nulls(fsl.nulls(), field.is_nullable())?;
557 Ok(
558 FixedSizeListArray::try_new(elements, *list_size as u32, validity, fsl.len())?
559 .into_array(),
560 )
561 }
562 DataType::ListView(elem_field) => {
563 let list = array.as_list_view::<i32>();
564 let elements = self
565 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
566 let offsets = list.offsets().clone().into_array();
567 let sizes = list.sizes().clone().into_array();
568 let validity = nulls(list.nulls(), field.is_nullable())?;
569 Ok(ListViewArray::try_new(elements, offsets, sizes, validity)?.into_array())
570 }
571 DataType::LargeListView(elem_field) => {
572 let list = array.as_list_view::<i64>();
573 let elements = self
574 .from_arrow_array(ArrowArrayRef::clone(list.values()), elem_field.as_ref())?;
575 let offsets = list.offsets().clone().into_array();
576 let sizes = list.sizes().clone().into_array();
577 let validity = nulls(list.nulls(), field.is_nullable())?;
578 Ok(ListViewArray::try_new(elements, offsets, sizes, validity)?.into_array())
579 }
580 _ => ArrayRef::from_arrow(array.as_ref(), field.is_nullable()),
581 }
582 }
583}
584
585pub(crate) fn has_valid_extension_type<E: ExtensionType>(field: &Field) -> bool {
589 if field.extension_type_name() != Some(E::NAME) {
590 return false;
591 }
592
593 E::try_new_from_field_metadata(field.data_type(), field.metadata()).is_ok()
594}
595
596impl SessionVar for ArrowSession {
597 fn as_any(&self) -> &dyn Any {
598 self
599 }
600
601 fn as_any_mut(&mut self) -> &mut dyn Any {
602 self
603 }
604}
605
606pub trait ArrowSessionExt: SessionExt {
608 fn arrow(&self) -> SessionGuard<'_, ArrowSession>;
610}
611
612impl<S: SessionExt> ArrowSessionExt for S {
613 fn arrow(&self) -> SessionGuard<'_, ArrowSession> {
614 self.get::<ArrowSession>()
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use std::sync::Arc;
621
622 use arrow_array::FixedSizeBinaryArray;
623 use arrow_array::cast::AsArray;
624 use arrow_schema::DataType;
625 use arrow_schema::Field;
626 use arrow_schema::extension::Uuid as ArrowUuid;
627 use vortex_array::VortexSessionExecute;
628 use vortex_array::array_session;
629 use vortex_array::dtype::DType;
630 use vortex_array::dtype::FieldName;
631 use vortex_array::dtype::Nullability;
632 use vortex_array::dtype::PType;
633 use vortex_array::dtype::StructFields;
634 use vortex_array::dtype::extension::ExtDType;
635 use vortex_array::dtype::extension::ExtVTable;
636 use vortex_array::extension::uuid::Uuid;
637 use vortex_array::extension::uuid::UuidMetadata;
638 use vortex_error::VortexResult;
639
640 use super::*;
641
642 fn uuid_dtype(nullable: bool) -> DType {
643 let storage = DType::FixedSizeList(
644 Arc::new(DType::Primitive(PType::U8, Nullability::NonNullable)),
645 16,
646 nullable.into(),
647 );
648 DType::Extension(
649 ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage)
650 .expect("uuid ext dtype")
651 .erased(),
652 )
653 }
654
655 #[test]
656 fn to_arrow_field_top_level_uuid_carries_extension_metadata() -> VortexResult<()> {
657 let session = ArrowSession::default();
658 let field = session.to_arrow_field("id", &uuid_dtype(false))?;
659 assert!(has_valid_extension_type::<ArrowUuid>(&field));
660 Ok(())
661 }
662
663 #[test]
664 fn to_arrow_field_struct_with_nested_uuid_preserves_metadata() -> VortexResult<()> {
665 let session = ArrowSession::default();
666 let dtype = DType::Struct(
667 StructFields::from_iter([(FieldName::from("id"), uuid_dtype(false))]),
668 Nullability::NonNullable,
669 );
670 let field = session.to_arrow_field("row", &dtype)?;
671 let DataType::Struct(inner) = field.data_type() else {
672 panic!("expected Struct, got {:?}", field.data_type());
673 };
674 assert_eq!(inner.len(), 1);
675 assert_eq!(inner[0].data_type(), &DataType::FixedSizeBinary(16));
676 assert!(has_valid_extension_type::<ArrowUuid>(&inner[0]));
677 Ok(())
678 }
679
680 #[test]
681 fn to_arrow_field_list_of_uuid_preserves_metadata() -> VortexResult<()> {
682 let session = ArrowSession::default();
683 let dtype = DType::List(Arc::new(uuid_dtype(true)), Nullability::NonNullable);
684 let field = session.to_arrow_field("ids", &dtype)?;
685 let DataType::List(elem) = field.data_type() else {
686 panic!("expected List, got {:?}", field.data_type());
687 };
688 assert!(has_valid_extension_type::<ArrowUuid>(elem));
689 Ok(())
690 }
691
692 #[test]
693 fn to_arrow_field_fixed_size_list_of_uuid_preserves_metadata() -> VortexResult<()> {
694 let session = ArrowSession::default();
695 let dtype = DType::FixedSizeList(Arc::new(uuid_dtype(false)), 3, Nullability::NonNullable);
696 let field = session.to_arrow_field("triple", &dtype)?;
697 let DataType::FixedSizeList(elem, size) = field.data_type() else {
698 panic!("expected FixedSizeList, got {:?}", field.data_type());
699 };
700 assert_eq!(*size, 3);
701 assert!(has_valid_extension_type::<ArrowUuid>(elem));
702 Ok(())
703 }
704
705 #[test]
706 fn to_arrow_schema_struct_of_struct_uuid() -> VortexResult<()> {
707 let session = ArrowSession::default();
708 let inner = DType::Struct(
709 StructFields::from_iter([(FieldName::from("id"), uuid_dtype(true))]),
710 Nullability::NonNullable,
711 );
712 let outer = DType::Struct(
713 StructFields::from_iter([(FieldName::from("payload"), inner)]),
714 Nullability::NonNullable,
715 );
716 let schema = session.to_arrow_schema(&outer)?;
717 let payload = schema.field(0);
718 let DataType::Struct(inner_fields) = payload.data_type() else {
719 panic!("expected Struct, got {:?}", payload.data_type());
720 };
721 assert!(has_valid_extension_type::<ArrowUuid>(&inner_fields[0]));
722 Ok(())
723 }
724
725 #[test]
726 fn from_arrow_field_recurses_into_nested_uuid() -> VortexResult<()> {
727 let session = ArrowSession::default();
728 let mut elem = Field::new("item", DataType::FixedSizeBinary(16), false);
729 elem.try_with_extension_type(ArrowUuid)?;
730 let outer = Field::new("ids", DataType::List(Arc::new(elem)), false);
731
732 let dtype = session.from_arrow_field(&outer)?;
733 let DType::List(inner_dt, _) = dtype else {
734 panic!("expected List dtype, got {dtype}");
735 };
736 assert!(
737 matches!(inner_dt.as_ref(), DType::Extension(ext) if ext.id() == Uuid.id()),
738 "expected Uuid extension element, got {inner_dt}",
739 );
740 Ok(())
741 }
742
743 #[test]
744 fn schema_roundtrip_preserves_nested_uuid() -> VortexResult<()> {
745 let session = ArrowSession::default();
746 let dtype = DType::Struct(
747 StructFields::from_iter([
748 (FieldName::from("id"), uuid_dtype(false)),
749 (
750 FieldName::from("ids"),
751 DType::List(Arc::new(uuid_dtype(true)), Nullability::NonNullable),
752 ),
753 ]),
754 Nullability::NonNullable,
755 );
756 let schema = session.to_arrow_schema(&dtype)?;
757 let roundtripped = session.from_arrow_schema(&schema)?;
758 assert_eq!(roundtripped, dtype);
759 Ok(())
760 }
761
762 #[test]
763 fn execute_arrow_target_none_preserves_top_level_uuid_metadata() -> VortexResult<()> {
764 let vortex_session = array_session();
765 let mut ctx = vortex_session.create_execution_ctx();
766 let session = vortex_session.arrow();
767
768 let mut field = Field::new("id", DataType::FixedSizeBinary(16), false);
769 field.try_with_extension_type(ArrowUuid)?;
770 let arrow_array: ArrowArrayRef = Arc::new(FixedSizeBinaryArray::try_from_iter(
771 [*b"0123456789abcdef", *b"fedcba9876543210"].into_iter(),
772 )?);
773
774 let vortex_array = session.from_arrow_array(arrow_array, &field)?;
775
776 let vortex_ext = vortex_array.dtype().as_extension();
777 assert!(vortex_ext.is::<Uuid>());
778
779 let exported = session.execute_arrow(vortex_array, None, &mut ctx)?;
780 assert_eq!(exported.data_type(), &DataType::FixedSizeBinary(16));
781 let fsb = exported.as_fixed_size_binary();
782 assert_eq!(fsb.len(), 2);
783 assert_eq!(fsb.value(0), b"0123456789abcdef");
784 assert_eq!(fsb.value(1), b"fedcba9876543210");
785 Ok(())
786 }
787}