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::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
72/// Outcome of a successful call to [`ArrowExportVTable::execute_arrow`].
73///
74/// Plugins that don't handle the supplied array return [`Unsupported`][Self::Unsupported]
75/// with ownership of the input so the session can probe the next plugin or fall back to the
76/// canonical path. Errors are propagated through [`VortexResult`].
77pub enum ArrowExport {
78    /// The plugin does not handle this input; the session may try another plugin.
79    Unsupported(ArrayRef),
80    /// A successful export.
81    Exported(ArrowArrayRef),
82}
83
84/// Outcome of a successful call to [`ArrowImportVTable::from_arrow_array`].
85///
86/// Plugins that don't handle the supplied array return [`Unsupported`][Self::Unsupported]
87/// with ownership of the input so the session can probe the next plugin or fall back to the
88/// canonical path. Errors are propagated through [`VortexResult`].
89pub enum ArrowImport {
90    /// The plugin does not handle this input; the session may try another plugin.
91    Unsupported(ArrowArrayRef),
92    /// A successful import.
93    Imported(ArrayRef),
94}
95
96/// Plugin layer for exporting a Vortex array to an Arrow extension type.
97///
98/// This is purely an implementation trait, its methods should not be called directly. Instead,
99/// use the methods on [`ArrowSession`].
100pub trait ArrowExportVTable: 'static + Send + Sync + Debug {
101    /// The Arrow extension ID this plugin produces.
102    fn arrow_ext_id(&self) -> Id;
103
104    /// The Vortex array or extension ID this plugin maps from. Used only for inference by
105    /// [`ArrowSession::to_arrow_field`] / [`ArrowSession::to_arrow_schema`]; never as a
106    /// dispatch key for [`execute_arrow`][Self::execute_arrow].
107    fn vortex_id(&self) -> Id;
108
109    /// Build the Arrow [`Field`] this plugin produces for the given Vortex extension
110    /// `dtype`. Used during schema inference.
111    fn to_arrow_field(
112        &self,
113        name: &str,
114        dtype: &DType,
115        session: &ArrowSession,
116    ) -> VortexResult<Option<Field>>;
117
118    /// Convert a Vortex array into an Arrow array shaped to `target`.
119    ///
120    /// Returns ownership of `array` via [`ArrowExport::Unsupported`] when the plugin cannot
121    /// handle the input.
122    fn execute_arrow(
123        &self,
124        array: ArrayRef,
125        target: &Field,
126        ctx: &mut ExecutionCtx,
127    ) -> VortexResult<ArrowExport>;
128}
129
130/// Plugin layer for importing an Arrow extension-typed array into a Vortex array.
131///
132/// Plugins are dispatched by `arrow_ext_id`.
133///
134/// This is purely an implementation trait, its methods should not be called directly. Instead,
135/// use the methods on [`ArrowSession`].
136pub trait ArrowImportVTable: 'static + Send + Sync + Debug {
137    /// The Arrow extension name this plugin handles.
138    fn arrow_ext_id(&self) -> Id;
139
140    /// Build the Vortex [`DType`] that corresponds to `field` (which carries this plugin's
141    /// Arrow extension metadata).
142    #[allow(clippy::wrong_self_convention)]
143    fn from_arrow_field(&self, field: &Field) -> VortexResult<Option<DType>>;
144
145    /// Convert an Arrow array into a Vortex array of `dtype`.
146    ///
147    /// Returns ownership of `array` via [`ArrowImport::Unsupported`] when the plugin cannot
148    /// handle the input.
149    #[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/// Session-scoped registry of Arrow extension plugins.
162///
163/// Exporters are stored in two indices: one keyed by Arrow extension Id (used for
164/// `execute_arrow` dispatch) and one keyed by Vortex extension Id (used **only** by
165/// `to_arrow_field` / `to_arrow_schema` inference, when callers need to translate a Vortex
166/// extension `DType` into an Arrow `Field` with no target schema in hand). Importers are
167/// keyed by Arrow extension name. The default session pre-registers the builtin UUID
168/// plugin; temporal extensions are handled by the canonical Arrow ↔ Vortex path and do not
169/// need plugins.
170#[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    /// Register an [`ArrowExportVTable`] under its target Arrow extension Id (for dispatch)
194    /// and its source Vortex extension Id (for schema inference).
195    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    /// Register an [`ArrowImportVTable`] under its source Arrow extension name.
205    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    /// Build the Arrow [`Field`] for a Vortex [`DType`].
224    ///
225    /// For [`DType::Extension`]s, plugins registered against the extension's `Id`
226    /// are tried in registration order; the first plugin to return `Some(field)` wins.
227    pub fn to_arrow_field(&self, name: &str, dtype: &DType) -> VortexResult<Field> {
228        // Handle the structural encodings, which may have recursive types
229        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                // TODO(Adam): This currently encodes information about parquet-variant
269                // at this level. Variant's complexity with being an essentially logical type
270                // with multiple physical layout complicates handling this correctly.
271                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    /// Build the Arrow [`Schema`] for a Vortex top-level [`DType::Struct`], dispatching
299    /// extension fields through registered export plugins for inference. Nested
300    /// extensions are preserved via [`Self::to_arrow_field`].
301    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    /// Build the Vortex [`DType`] for an Arrow [`Field`].
313    ///
314    /// Plugins registered against the field's Arrow extension name are tried in
315    /// registration order; the first plugin to return `Some(dtype)` wins. If none
316    /// match (or all return `None`), recurses into container types ([`DataType::List`]
317    /// family, [`DataType::FixedSizeList`], [`DataType::Struct`]) so extension metadata
318    /// on nested element/struct fields is preserved. Leaf types use the canonical
319    /// Arrow → Vortex mapping via [`DType::try_from_arrow`].
320    #[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    /// Build the Vortex [`DType`] for an Arrow [`Schema`], dispatching extension fields
357    /// through registered import plugins. The result is a top-level non-nullable struct
358    /// matching the schema's fields.
359    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    /// Decode an Arrow [`RecordBatch`] into a Vortex struct array, dispatching each
375    /// extension column through its registered import plugin.
376    ///
377    /// `schema` is the authoritative Arrow schema used for dispatch — the columns are
378    /// consumed positionally. Pass an external schema (rather than relying on
379    /// `batch.schema()`) when upstream DataFusion plumbing may have stripped Field-level
380    /// extension metadata from the runtime RecordBatch.
381    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    /// Execute a Vortex array into an Arrow array.
407    ///
408    /// If `target` carries an `ARROW:extension:name`, the plugin registry is probed for one that
409    /// can support executing to the target extension type.
410    ///
411    /// With `target = None` the fallback path picks the array's preferred Arrow physical type
412    /// and executes directly into that, ignoring extension types.
413    #[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        // NOTE(aduffy): this looks strange, but we do this to keep target_field as &Field so
421        //  we can avoid cloning target when it is provided. It contains a HashMap internally that
422        //  can be expensive to copy.
423        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            // There can be multiple plugins that report support for a particular extension type.
435            // We try them in order until one of them reports a successful conversion.
436            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    /// Decode an Arrow array into a Vortex array.
472    ///
473    /// Routes through the registered import plugin if `field` carries an Arrow extension
474    /// name we recognize, probing each plugin in registration order until one handles the
475    /// input or all return [`ArrowImport::Unsupported`]. Otherwise recurses into container
476    /// arrays ([`arrow_array::StructArray`], [`arrow_array::GenericListArray`],
477    /// [`arrow_array::FixedSizeListArray`], [`arrow_array::GenericListViewArray`]) so
478    /// extension fields nested inside containers reach their importers; leaf types fall
479    /// through to the canonical Arrow → Vortex array conversion.
480    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    /// Recurse into Arrow container arrays so nested fields with extension metadata reach
500    /// their importers, falling through to [`ArrayRef::from_arrow`] for leaf types.
501    #[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                        // Arrow pushes nulls into non-nullable fields; strip before recursing
521                        // so Vortex's stricter validity invariants are upheld.
522                        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
585// NOTE(aduffy): We should remove this once we bump Arrow to 0.59.0. This is replicating the
586//  `Field::has_valid_extension_type` method on Arrow added in 58.2.0, we polyfill it here so that
587//  this crate can build with minimal-versions declared.
588pub(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
606/// Extension trait for accessing the [`ArrowSession`] on a Vortex session.
607pub trait ArrowSessionExt: SessionExt {
608    /// Get the Arrow session.
609    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}