Skip to main content

vortex_arrow/
session.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Plugin layer for moving Arrow extension types in and out of Vortex.
5//!
6//! Vortex's canonical Arrow conversion (see [`crate::dtype`] and the executor in
7//! [`crate::executor`]) handles every non-extension Arrow type and the builtin temporal
8//! extensions. The plugins registered here cover the remaining case: **Arrow extension types**.
9//!
10//! * An [`ArrowExportVTable`] is dispatched purely by the **target Arrow extension Id** —
11//!   the plugin is selected when the caller asks for an Arrow [`Field`] carrying matching
12//!   `ARROW:extension:name` metadata. The Vortex source dtype/encoding is irrelevant to
13//!   dispatch.
14//! * An [`ArrowImportVTable`] is dispatched by the **source Arrow extension name** carried
15//!   on the incoming [`Field`]. The plugin is responsible for both preserving extension
16//!   identity and re-encoding storage if needed (e.g. Arrow `FixedSizeBinary[16]` for UUID
17//!   becomes Vortex `FixedSizeList<u8; 16>`).
18//!
19//! Multiple plugins may register against the same key. They are tried in registration order;
20//! each may return [`ArrowExport::Unsupported`] / [`ArrowImport::Unsupported`] to defer to
21//! the next.
22
23use 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
73/// Outcome of a successful call to [`ArrowExportVTable::execute_arrow`].
74///
75/// Plugins that don't handle the supplied array return [`Unsupported`][Self::Unsupported]
76/// with ownership of the input so the session can probe the next plugin or fall back to the
77/// canonical path. Errors are propagated through [`VortexResult`].
78pub enum ArrowExport {
79    /// The plugin does not handle this input; the session may try another plugin.
80    Unsupported(ArrayRef),
81    /// A successful export.
82    Exported(ArrowArrayRef),
83}
84
85/// Outcome of a successful call to [`ArrowImportVTable::from_arrow_array`].
86///
87/// Plugins that don't handle the supplied array return [`Unsupported`][Self::Unsupported]
88/// with ownership of the input so the session can probe the next plugin or fall back to the
89/// canonical path. Errors are propagated through [`VortexResult`].
90pub enum ArrowImport {
91    /// The plugin does not handle this input; the session may try another plugin.
92    Unsupported(ArrowArrayRef),
93    /// A successful import.
94    Imported(ArrayRef),
95}
96
97/// Plugin layer for exporting a Vortex array to an Arrow extension type.
98///
99/// This is purely an implementation trait, its methods should not be called directly. Instead,
100/// use the methods on [`ArrowSession`].
101pub trait ArrowExportVTable: 'static + Send + Sync + Debug {
102    /// The Arrow extension ID this plugin produces.
103    fn arrow_ext_id(&self) -> Id;
104
105    /// The Vortex array or extension ID this plugin maps from. Used only for inference by
106    /// [`ArrowSession::to_arrow_field`] / [`ArrowSession::to_arrow_schema`]; never as a
107    /// dispatch key for [`execute_arrow`][Self::execute_arrow].
108    fn vortex_id(&self) -> Id;
109
110    /// Build the Arrow [`Field`] this plugin produces for the given Vortex extension
111    /// `dtype`. Used during schema inference.
112    fn to_arrow_field(
113        &self,
114        name: &str,
115        dtype: &DType,
116        session: &ArrowSession,
117    ) -> VortexResult<Option<Field>>;
118
119    /// Convert a Vortex array into an Arrow array shaped to `target`.
120    ///
121    /// Returns ownership of `array` via [`ArrowExport::Unsupported`] when the plugin cannot
122    /// handle the input.
123    fn execute_arrow(
124        &self,
125        array: ArrayRef,
126        target: &Field,
127        ctx: &mut ExecutionCtx,
128    ) -> VortexResult<ArrowExport>;
129}
130
131/// Plugin layer for importing an Arrow extension-typed array into a Vortex array.
132///
133/// Plugins are dispatched by `arrow_ext_id`.
134///
135/// This is purely an implementation trait, its methods should not be called directly. Instead,
136/// use the methods on [`ArrowSession`].
137pub trait ArrowImportVTable: 'static + Send + Sync + Debug {
138    /// The Arrow extension name this plugin handles.
139    fn arrow_ext_id(&self) -> Id;
140
141    /// Build the Vortex [`DType`] that corresponds to `field` (which carries this plugin's
142    /// Arrow extension metadata).
143    #[allow(clippy::wrong_self_convention)]
144    fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>>;
145
146    /// Convert an Arrow array into a Vortex array of `dtype`.
147    ///
148    /// Returns ownership of `array` via [`ArrowImport::Unsupported`] when the plugin cannot
149    /// handle the input.
150    #[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
162/// Registry of Arrow exporters, keyed by target Arrow extension [`Id`].
163type ArrowExporterRegistry = ArcSwapMap<Id, Arc<[ArrowExportVTableRef]>>;
164/// Registry of Arrow exporters, keyed by source Vortex extension [`ExtId`].
165type VortexExporterRegistry = ArcSwapMap<ExtId, Arc<[ArrowExportVTableRef]>>;
166/// Registry of Arrow importers, keyed by source Arrow extension [`Id`].
167type ArrowImporterRegistry = ArcSwapMap<Id, Arc<[ArrowImportVTableRef]>>;
168
169/// Session-scoped registry of Arrow extension plugins.
170///
171/// Exporters are stored in two indices: one keyed by Arrow extension Id (used for
172/// `execute_arrow` dispatch) and one keyed by Vortex extension Id (used **only** by
173/// `to_arrow_field` / `to_arrow_schema` inference, when callers need to translate a Vortex
174/// extension `DType` into an Arrow `Field` with no target schema in hand). Importers are
175/// keyed by Arrow extension name. The default session pre-registers the builtin UUID
176/// plugin; temporal extensions are handled by the canonical Arrow ↔ Vortex path and do not
177/// need plugins.
178#[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    /// Register an [`ArrowExportVTable`] under its target Arrow extension Id (for dispatch)
202    /// and its source Vortex extension Id (for schema inference).
203    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    /// Register an [`ArrowImportVTable`] under its source Arrow extension name.
213    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    /// Build the Arrow [`Field`] for a Vortex [`DType`].
232    ///
233    /// For [`DType::Extension`]s, plugins registered against the extension's `Id`
234    /// are tried in registration order; the first plugin to return `Some(field)` wins.
235    pub fn to_arrow_field(&self, name: &str, dtype: &DType) -> VortexResult<Field> {
236        // Handle the structural encodings, which may have recursive types
237        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                // TODO(Adam): This currently encodes information about parquet-variant
287                // at this level. Variant's complexity with being an essentially logical type
288                // with multiple physical layout complicates handling this correctly.
289                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    /// Build the Arrow [`Schema`] for a Vortex top-level [`DType::Struct`], dispatching
317    /// extension fields through registered export plugins for inference. Nested
318    /// extensions are preserved via [`Self::to_arrow_field`].
319    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    /// Build the Vortex [`DType`] for an Arrow [`Field`].
331    ///
332    /// Plugins registered against the field's Arrow extension name are tried in
333    /// registration order; the first plugin to return `Some(dtype)` wins. If none
334    /// match (or all return `None`), recurses into container types ([`DataType::List`]
335    /// family, [`DataType::FixedSizeList`], [`DataType::Struct`]) so extension metadata
336    /// on nested element/struct fields is preserved. Leaf types use the canonical
337    /// Arrow → Vortex mapping via [`DType::try_from_arrow`].
338    #[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    /// Build the Vortex [`DType`] for an Arrow [`Schema`], dispatching extension fields
401    /// through registered import plugins. The result is a top-level non-nullable struct
402    /// matching the schema's fields.
403    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    /// Decode an Arrow [`RecordBatch`] into a Vortex struct array, dispatching each
419    /// extension column through its registered import plugin.
420    ///
421    /// `schema` is the authoritative Arrow schema used for dispatch — the columns are
422    /// consumed positionally. Pass an external schema (rather than relying on
423    /// `batch.schema()`) when upstream DataFusion plumbing may have stripped Field-level
424    /// extension metadata from the runtime RecordBatch.
425    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    /// Execute a Vortex array into an Arrow array.
451    ///
452    /// If `target` carries an `ARROW:extension:name`, the plugin registry is probed for one that
453    /// can support executing to the target extension type.
454    ///
455    /// With `target = None` the fallback path picks the array's preferred Arrow physical type
456    /// and executes directly into that, ignoring extension types.
457    #[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        // NOTE(aduffy): this looks strange, but we do this to keep target_field as &Field so
465        //  we can avoid cloning target when it is provided. It contains a HashMap internally that
466        //  can be expensive to copy.
467        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            // There can be multiple plugins that report support for a particular extension type.
479            // We try them in order until one of them reports a successful conversion.
480            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    /// Decode an Arrow array into a Vortex array.
516    ///
517    /// Routes through the registered import plugin if `field` carries an Arrow extension
518    /// name we recognize, probing each plugin in registration order until one handles the
519    /// input or all return [`ArrowImport::Unsupported`]. Otherwise recurses into container
520    /// arrays ([`arrow_array::StructArray`], [`arrow_array::GenericListArray`],
521    /// [`arrow_array::FixedSizeListArray`], [`arrow_array::GenericListViewArray`]) so
522    /// extension fields nested inside containers reach their importers; leaf types fall
523    /// through to the canonical Arrow → Vortex array conversion.
524    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    /// Recurse into Arrow container arrays so nested fields with extension metadata reach
544    /// their importers, falling through to [`ArrayRef::from_arrow`] for leaf types.
545    #[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                        // Arrow pushes nulls into non-nullable fields; strip before recursing
565                        // so Vortex's stricter validity invariants are upheld.
566                        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
641// NOTE(aduffy): We should remove this once we bump Arrow to 0.59.0. This is replicating the
642//  `Field::has_valid_extension_type` method on Arrow added in 58.2.0, we polyfill it here so that
643//  this crate can build with minimal-versions declared.
644pub(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
662/// Extension trait for accessing the [`ArrowSession`] on a Vortex session.
663pub trait ArrowSessionExt: SessionExt {
664    /// Get the Arrow session.
665    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}