Skip to main content

vortex_arrow/
dtype.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Convert between Vortex [`vortex_array::dtype::DType`] and Apache Arrow [`arrow_schema::DataType`].
5//!
6//! Apache Arrow's type system includes physical information, which could lead to ambiguities as
7//! Vortex treats encodings as separate from logical types.
8//!
9//! The conversions in this module are "naive": every logical type is encoded in its simplest
10//! corresponding Arrow type, and Arrow extension types (other than the builtin temporal types and
11//! Parquet Variant) are not understood. The authoritative, plugin-aware conversion entry point is
12//! [`ArrowSession`](crate::ArrowSession), through its `to_arrow_schema` / `to_arrow_datatype` /
13//! `from_arrow_schema` / `from_arrow_field` / `from_arrow_datatype` methods.
14//!
15//! For this reason, it's recommended to do as much computation as possible within Vortex, and then
16//! materialize an Arrow ArrayRef at the very end of the processing chain.
17
18use std::sync::Arc;
19
20use arrow_schema::DataType;
21use arrow_schema::Field;
22use arrow_schema::FieldRef;
23use arrow_schema::Fields;
24use arrow_schema::TimeUnit as ArrowTimeUnit;
25use vortex_array::dtype::DType;
26use vortex_array::dtype::DecimalDType;
27use vortex_array::dtype::FieldName;
28use vortex_array::dtype::Nullability;
29use vortex_array::dtype::PType;
30use vortex_array::dtype::StructFields;
31use vortex_array::extension::datetime::AnyTemporal;
32use vortex_array::extension::datetime::Date;
33use vortex_array::extension::datetime::TemporalMetadata;
34use vortex_array::extension::datetime::Time;
35use vortex_array::extension::datetime::TimeUnit;
36use vortex_array::extension::datetime::Timestamp;
37use vortex_error::VortexResult;
38use vortex_error::vortex_bail;
39use vortex_error::vortex_ensure;
40use vortex_error::vortex_ensure_eq;
41use vortex_error::vortex_err;
42use vortex_error::vortex_panic;
43
44/// Convert an Arrow [`ArrowTimeUnit`] to a Vortex [`TimeUnit`].
45pub(crate) fn from_arrow_time_unit(value: ArrowTimeUnit) -> TimeUnit {
46    match value {
47        ArrowTimeUnit::Second => TimeUnit::Seconds,
48        ArrowTimeUnit::Millisecond => TimeUnit::Milliseconds,
49        ArrowTimeUnit::Microsecond => TimeUnit::Microseconds,
50        ArrowTimeUnit::Nanosecond => TimeUnit::Nanoseconds,
51    }
52}
53
54/// Convert a Vortex [`TimeUnit`] to an Arrow [`ArrowTimeUnit`].
55///
56/// # Errors
57///
58/// Returns an error for units with no Arrow equivalent (e.g. [`TimeUnit::Days`]).
59pub(crate) fn to_arrow_time_unit(value: TimeUnit) -> VortexResult<ArrowTimeUnit> {
60    Ok(match value {
61        TimeUnit::Seconds => ArrowTimeUnit::Second,
62        TimeUnit::Milliseconds => ArrowTimeUnit::Millisecond,
63        TimeUnit::Microseconds => ArrowTimeUnit::Microsecond,
64        TimeUnit::Nanoseconds => ArrowTimeUnit::Nanosecond,
65        _ => vortex_bail!("Cannot convert {value} to Arrow TimeUnit"),
66    })
67}
68
69/// Naive conversion of an Arrow [`DataType`] to the Vortex [`PType`] it stores.
70pub(crate) fn ptype_from_arrow(value: &DataType) -> VortexResult<PType> {
71    match value {
72        DataType::Int8 => Ok(PType::I8),
73        DataType::Int16 => Ok(PType::I16),
74        DataType::Int32 => Ok(PType::I32),
75        DataType::Int64 => Ok(PType::I64),
76        DataType::UInt8 => Ok(PType::U8),
77        DataType::UInt16 => Ok(PType::U16),
78        DataType::UInt32 => Ok(PType::U32),
79        DataType::UInt64 => Ok(PType::U64),
80        DataType::Float16 => Ok(PType::F16),
81        DataType::Float32 => Ok(PType::F32),
82        DataType::Float64 => Ok(PType::F64),
83        _ => Err(vortex_err!(
84            "Arrow datatype {:?} cannot be converted to ptype",
85            value
86        )),
87    }
88}
89
90/// Naive conversion of an Arrow [`DataType`] to a Vortex [`DType`], with no awareness of Arrow
91/// extension plugins.
92pub(crate) fn from_arrow_data_type(
93    data_type: &DataType,
94    nullability: Nullability,
95) -> VortexResult<DType> {
96    if data_type.is_integer() || data_type.is_floating() {
97        return Ok(DType::Primitive(ptype_from_arrow(data_type)?, nullability));
98    }
99
100    Ok(match data_type {
101        DataType::Null => DType::Null,
102        DataType::Decimal32(precision, scale)
103        | DataType::Decimal64(precision, scale)
104        | DataType::Decimal128(precision, scale)
105        | DataType::Decimal256(precision, scale) => {
106            DType::Decimal(DecimalDType::try_new(*precision, *scale)?, nullability)
107        }
108        DataType::Boolean => DType::Bool(nullability),
109        DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => DType::Utf8(nullability),
110        DataType::Binary | DataType::LargeBinary | DataType::BinaryView => {
111            DType::Binary(nullability)
112        }
113        DataType::Date32 => DType::Extension(Date::new(TimeUnit::Days, nullability).erased()),
114        DataType::Date64 => {
115            DType::Extension(Date::new(TimeUnit::Milliseconds, nullability).erased())
116        }
117        DataType::Time32(unit) => {
118            DType::Extension(Time::new(from_arrow_time_unit(*unit), nullability).erased())
119        }
120        DataType::Time64(unit) => {
121            DType::Extension(Time::new(from_arrow_time_unit(*unit), nullability).erased())
122        }
123        DataType::Timestamp(unit, tz) => DType::Extension(
124            Timestamp::new_with_tz(from_arrow_time_unit(*unit), tz.clone(), nullability).erased(),
125        ),
126        DataType::List(e)
127        | DataType::LargeList(e)
128        | DataType::ListView(e)
129        | DataType::LargeListView(e) => {
130            DType::List(Arc::new(from_arrow_field_naive(e.as_ref())?), nullability)
131        }
132        DataType::FixedSizeList(e, size) => DType::FixedSizeList(
133            Arc::new(from_arrow_field_naive(e.as_ref())?),
134            *size as u32,
135            nullability,
136        ),
137        DataType::Struct(f) => DType::Struct(from_arrow_fields_naive(f)?, nullability),
138        DataType::Map(entries, keys_sorted) => {
139            vortex_ensure!(
140                !entries.is_nullable(),
141                "Arrow map entries field must be non-nullable"
142            );
143            let DataType::Struct(fields) = entries.data_type() else {
144                vortex_bail!(
145                    "Arrow map entries field must have Struct type, got {:?}",
146                    entries.data_type()
147                );
148            };
149            vortex_ensure_eq!(
150                fields.len(),
151                2,
152                InvalidArgument: "Arrow map entries struct must contain exactly two fields"
153            );
154            let key = &fields[0];
155            let value = &fields[1];
156            vortex_ensure!(
157                !key.is_nullable(),
158                "Arrow map key field must be non-nullable"
159            );
160            DType::map(
161                from_arrow_field_naive(key.as_ref())?,
162                from_arrow_field_naive(value.as_ref())?,
163                *keys_sorted,
164                nullability,
165            )?
166        }
167        DataType::Dictionary(_, value_type) => {
168            from_arrow_data_type(value_type.as_ref(), nullability)?
169        }
170        DataType::RunEndEncoded(_, value_type) => {
171            from_arrow_data_type(value_type.data_type(), nullability)?
172        }
173        _ => vortex_bail!("Arrow data type not supported: {data_type:?}"),
174    })
175}
176
177/// Naive conversion of an Arrow [`Field`] to a Vortex [`DType`]. Only the builtin
178/// `arrow.parquet.variant` extension metadata is recognized.
179pub(crate) fn from_arrow_field_naive(field: &Field) -> VortexResult<DType> {
180    if field
181        .metadata()
182        .get("ARROW:extension:name")
183        .map(|s| s.as_str())
184        == Some("arrow.parquet.variant")
185    {
186        return Ok(DType::Variant(field.is_nullable().into()));
187    }
188    from_arrow_data_type(field.data_type(), field.is_nullable().into())
189}
190
191/// Naive conversion of Arrow [`Fields`] to Vortex [`StructFields`].
192pub(crate) fn from_arrow_fields_naive(fields: &Fields) -> VortexResult<StructFields> {
193    fields
194        .into_iter()
195        .map(|f| {
196            Ok((
197                FieldName::from(f.name().as_str()),
198                from_arrow_field_naive(f.as_ref())?,
199            ))
200        })
201        .collect::<VortexResult<StructFields>>()
202}
203
204/// Naive conversion from a Vortex `DType` to the nearest Arrow physical data type.
205pub(crate) fn to_data_type_naive(dtype: &DType) -> VortexResult<DataType> {
206    Ok(match dtype {
207        DType::Null => DataType::Null,
208        DType::Bool(_) => DataType::Boolean,
209        DType::Primitive(ptype, _) => match ptype {
210            PType::U8 => DataType::UInt8,
211            PType::U16 => DataType::UInt16,
212            PType::U32 => DataType::UInt32,
213            PType::U64 => DataType::UInt64,
214            PType::I8 => DataType::Int8,
215            PType::I16 => DataType::Int16,
216            PType::I32 => DataType::Int32,
217            PType::I64 => DataType::Int64,
218            PType::F16 => DataType::Float16,
219            PType::F32 => DataType::Float32,
220            PType::F64 => DataType::Float64,
221        },
222        DType::Decimal(dt, _) => {
223            let precision = dt.precision();
224            let scale = dt.scale();
225
226            match precision {
227                // This code is commented out until DataFusion improves its support for smaller decimals.
228                // // DECIMAL32_MAX_PRECISION
229                // 0..=9 => DataType::Decimal32(precision, scale),
230                // // DECIMAL64_MAX_PRECISION
231                // 10..=18 => DataType::Decimal64(precision, scale),
232                // DECIMAL128_MAX_PRECISION
233                0..=38 => DataType::Decimal128(precision, scale),
234                // DECIMAL256_MAX_PRECISION
235                39.. => DataType::Decimal256(precision, scale),
236            }
237        }
238        DType::Utf8(_) => DataType::Utf8View,
239        DType::Binary(_) => DataType::BinaryView,
240        // There are four kinds of lists: List (32-bit offsets), Large List (64-bit), List View
241        // (32-bit), Large List View (64-bit). We cannot both guarantee zero-copy and commit to an
242        // Arrow dtype because we do not how large our offsets are.
243        DType::List(elem_dtype, _) => DataType::List(FieldRef::new(Field::new_list_field(
244            to_data_type_naive(elem_dtype)?,
245            elem_dtype.nullability().into(),
246        ))),
247        DType::FixedSizeList(elem_dtype, size, _) => DataType::FixedSizeList(
248            FieldRef::new(Field::new_list_field(
249                to_data_type_naive(elem_dtype)?,
250                elem_dtype.nullability().into(),
251            )),
252            *size as i32,
253        ),
254        DType::Map(map_dtype, _) => {
255            let key = Field::new("key", to_data_type_naive(&map_dtype.key_dtype())?, false);
256            let value_dtype = map_dtype.value_dtype();
257            let value = Field::new(
258                "value",
259                to_data_type_naive(&value_dtype)?,
260                value_dtype.is_nullable(),
261            );
262            let entries = Field::new_struct("entries", Fields::from(vec![key, value]), false);
263            DataType::Map(FieldRef::new(entries), map_dtype.keys_sorted())
264        }
265        DType::Struct(struct_dtype, _) => {
266            let mut fields = Vec::with_capacity(struct_dtype.names().len());
267            for (field_name, field_dt) in struct_dtype.names().iter().zip(struct_dtype.fields()) {
268                fields.push(FieldRef::from(Field::new(
269                    field_name.as_ref(),
270                    to_data_type_naive(&field_dt)?,
271                    field_dt.is_nullable(),
272                )));
273            }
274
275            DataType::Struct(Fields::from(fields))
276        }
277        DType::Union(..) => vortex_bail!("TODO(connor)[Union]: unimplemented"),
278        DType::Variant(_) => vortex_bail!(
279            "DType::Variant requires Arrow Field metadata; use to_arrow_schema or a Field helper"
280        ),
281        DType::Extension(ext_dtype) => {
282            // NOTE: Temporal are the only builtin and default-loaded extension types, and they map
283            // directly onto non-extension Arrow physical encodings. For this reason, we
284            // choose to special-case them as part of this function rather than implementing them
285            // as an import/export VTable.
286            if let Some(temporal) = ext_dtype.metadata_opt::<AnyTemporal>() {
287                return Ok(match temporal {
288                    TemporalMetadata::Timestamp(unit, tz) => {
289                        DataType::Timestamp(to_arrow_time_unit(*unit)?, tz.clone())
290                    }
291                    TemporalMetadata::Date(unit) => match unit {
292                        TimeUnit::Days => DataType::Date32,
293                        TimeUnit::Milliseconds => DataType::Date64,
294                        TimeUnit::Nanoseconds | TimeUnit::Microseconds | TimeUnit::Seconds => {
295                            vortex_panic!(InvalidArgument: "Invalid TimeUnit {} for {}", unit, ext_dtype.id())
296                        }
297                    },
298                    TemporalMetadata::Time(unit) => match unit {
299                        TimeUnit::Seconds => DataType::Time32(ArrowTimeUnit::Second),
300                        TimeUnit::Milliseconds => DataType::Time32(ArrowTimeUnit::Millisecond),
301                        TimeUnit::Microseconds => DataType::Time64(ArrowTimeUnit::Microsecond),
302                        TimeUnit::Nanoseconds => DataType::Time64(ArrowTimeUnit::Nanosecond),
303                        TimeUnit::Days => {
304                            vortex_panic!(InvalidArgument: "Invalid TimeUnit {} for {}", unit, ext_dtype.id())
305                        }
306                    },
307                });
308            };
309
310            vortex_bail!("Unsupported extension type \"{}\"", ext_dtype.id())
311        }
312    })
313}
314
315#[cfg(test)]
316mod test {
317    use arrow_schema::DataType;
318    use arrow_schema::Field;
319    use arrow_schema::FieldRef;
320    use arrow_schema::Fields;
321    use arrow_schema::Schema;
322    use rstest::fixture;
323    use rstest::rstest;
324    use vortex_array::dtype::DType;
325    use vortex_array::dtype::FieldName;
326    use vortex_array::dtype::FieldNames;
327    use vortex_array::dtype::Nullability;
328    use vortex_array::dtype::PType;
329    use vortex_array::dtype::StructFields;
330
331    use super::*;
332    use crate::ArrowSession;
333
334    #[test]
335    fn test_dtype_conversion_success() {
336        assert_eq!(to_data_type_naive(&DType::Null).unwrap(), DataType::Null);
337
338        assert_eq!(
339            to_data_type_naive(&DType::Bool(Nullability::NonNullable)).unwrap(),
340            DataType::Boolean
341        );
342
343        assert_eq!(
344            to_data_type_naive(&DType::Primitive(PType::U64, Nullability::NonNullable)).unwrap(),
345            DataType::UInt64
346        );
347
348        assert_eq!(
349            to_data_type_naive(&DType::Utf8(Nullability::NonNullable)).unwrap(),
350            DataType::Utf8View
351        );
352
353        assert_eq!(
354            to_data_type_naive(&DType::Binary(Nullability::NonNullable)).unwrap(),
355            DataType::BinaryView
356        );
357
358        assert_eq!(
359            to_data_type_naive(&DType::struct_(
360                [
361                    ("field_a", DType::Bool(false.into())),
362                    ("field_b", DType::Utf8(true.into()))
363                ],
364                Nullability::NonNullable,
365            ))
366            .unwrap(),
367            DataType::Struct(Fields::from(vec![
368                FieldRef::from(Field::new("field_a", DataType::Boolean, false)),
369                FieldRef::from(Field::new("field_b", DataType::Utf8View, true)),
370            ]))
371        );
372    }
373
374    #[rstest]
375    #[case(1, DataType::Decimal128(1, 0))]
376    #[case(38, DataType::Decimal128(38, 0))]
377    #[case(39, DataType::Decimal256(39, 0))]
378    #[case(76, DataType::Decimal256(76, 0))]
379    fn test_decimal_dtype_to_arrow(#[case] precision: u8, #[case] expected: DataType) {
380        use vortex_array::dtype::DecimalDType;
381
382        let dtype = DType::Decimal(DecimalDType::new(precision, 0), Nullability::NonNullable);
383        assert_eq!(to_data_type_naive(&dtype).unwrap(), expected);
384    }
385
386    #[rstest]
387    #[case::decimal32(DataType::Decimal32(1, 2))]
388    #[case::decimal64(DataType::Decimal64(1, 2))]
389    #[case::decimal128(DataType::Decimal128(1, 2))]
390    #[case::decimal256(DataType::Decimal256(1, 2))]
391    #[case::zero_precision(DataType::Decimal128(0, 0))]
392    #[case::excessive_precision(DataType::Decimal256(77, 0))]
393    fn test_malformed_decimal_dtype_from_arrow(#[case] data_type: DataType) {
394        assert!(from_arrow_data_type(&data_type, Nullability::Nullable).is_err());
395    }
396
397    #[test]
398    fn test_variant_dtype_to_arrow_dtype_errors() {
399        let err = to_data_type_naive(&DType::Variant(Nullability::NonNullable))
400            .unwrap_err()
401            .to_string();
402        assert!(err.contains("Variant"));
403    }
404
405    #[test]
406    fn infer_nullable_list_element() {
407        let list_non_nullable = DType::List(
408            Arc::new(DType::Primitive(PType::I64, Nullability::NonNullable)),
409            Nullability::Nullable,
410        );
411
412        let arrow_list_non_nullable = to_data_type_naive(&list_non_nullable).unwrap();
413
414        let list_nullable = DType::List(
415            Arc::new(DType::Primitive(PType::I64, Nullability::Nullable)),
416            Nullability::Nullable,
417        );
418        let arrow_list_nullable = to_data_type_naive(&list_nullable).unwrap();
419
420        assert_ne!(arrow_list_non_nullable, arrow_list_nullable);
421        assert_eq!(
422            arrow_list_nullable,
423            DataType::List(Arc::new(Field::new_list_field(DataType::Int64, true))),
424        );
425        assert_eq!(
426            arrow_list_non_nullable,
427            DataType::List(Arc::new(Field::new_list_field(DataType::Int64, false))),
428        );
429    }
430
431    #[fixture]
432    fn the_struct() -> StructFields {
433        StructFields::new(
434            FieldNames::from([
435                FieldName::from("field_a"),
436                FieldName::from("field_b"),
437                FieldName::from("field_c"),
438            ]),
439            vec![
440                DType::Bool(Nullability::NonNullable),
441                DType::Utf8(Nullability::NonNullable),
442                DType::Primitive(PType::I32, Nullability::Nullable),
443            ],
444        )
445    }
446
447    #[rstest]
448    fn test_schema_conversion(the_struct: StructFields) {
449        let schema_nonnull = DType::Struct(the_struct, Nullability::NonNullable);
450
451        assert_eq!(
452            ArrowSession::default()
453                .to_arrow_schema(&schema_nonnull)
454                .unwrap(),
455            Schema::new(Fields::from(vec![
456                Field::new("field_a", DataType::Boolean, false),
457                Field::new("field_b", DataType::Utf8View, false),
458                Field::new("field_c", DataType::Int32, true),
459            ]))
460        );
461    }
462
463    #[test]
464    fn test_unicode_field_names_roundtrip() {
465        // Regression test for https://github.com/vortex-data/vortex/issues/5979.
466
467        // Unicode characters in field names should survive an Arrow roundtrip without
468        // double-escaping.
469        let unicode_field_name = "\u{5}=A";
470        let original_dtype = DType::struct_(
471            [(
472                unicode_field_name,
473                DType::Primitive(PType::I8, Nullability::Nullable),
474            )],
475            Nullability::NonNullable,
476        );
477
478        let arrow_dtype = to_data_type_naive(&original_dtype).unwrap();
479        assert_eq!(
480            arrow_dtype,
481            DataType::Struct(Fields::from(vec![Field::new(
482                "\u{5}=A",
483                DataType::Int8,
484                true
485            )]))
486        );
487        let roundtripped_dtype =
488            from_arrow_data_type(&arrow_dtype, Nullability::NonNullable).unwrap();
489
490        assert_eq!(original_dtype, roundtripped_dtype);
491    }
492
493    #[test]
494    fn test_unicode_field_names_nested_roundtrip() {
495        // Regression test for https://github.com/vortex-data/vortex/issues/5979.
496
497        // Nested structs with unicode field names should also survive an Arrow roundtrip.
498        let inner_struct = DType::struct_(
499            [(
500                "\u{6}=inner",
501                DType::Primitive(PType::I32, Nullability::Nullable),
502            )],
503            Nullability::Nullable,
504        );
505        let original_dtype =
506            DType::struct_([("\u{7}=outer", inner_struct)], Nullability::NonNullable);
507
508        let arrow_dtype = to_data_type_naive(&original_dtype).unwrap();
509        assert_eq!(
510            arrow_dtype,
511            DataType::Struct(Fields::from(vec![Field::new(
512                "\u{7}=outer",
513                DataType::Struct(Fields::from(vec![Field::new(
514                    "\u{6}=inner",
515                    DataType::Int32,
516                    true
517                )])),
518                true
519            )]))
520        );
521        let roundtripped_dtype =
522            from_arrow_data_type(&arrow_dtype, Nullability::NonNullable).unwrap();
523
524        assert_eq!(original_dtype, roundtripped_dtype);
525    }
526
527    #[test]
528    fn map_dtype_roundtrip_uses_conventional_export_names_and_positional_import() -> VortexResult<()>
529    {
530        let dtype = DType::map(
531            DType::Primitive(PType::I32, Nullability::NonNullable),
532            DType::Utf8(Nullability::Nullable),
533            true,
534            Nullability::Nullable,
535        )?;
536
537        let arrow = to_data_type_naive(&dtype)?;
538        let DataType::Map(entries, keys_sorted) = &arrow else {
539            panic!("expected Map, got {arrow:?}");
540        };
541        assert!(*keys_sorted);
542        assert_eq!(entries.name(), "entries");
543        assert!(!entries.is_nullable());
544        let DataType::Struct(fields) = entries.data_type() else {
545            panic!("expected map entries to be a struct");
546        };
547        assert_eq!(fields[0].name(), "key");
548        assert!(!fields[0].is_nullable());
549        assert_eq!(fields[1].name(), "value");
550        assert!(fields[1].is_nullable());
551        assert_eq!(from_arrow_data_type(&arrow, Nullability::Nullable)?, dtype);
552
553        let positional = DataType::Map(
554            Arc::new(Field::new_struct(
555                "anything",
556                Fields::from(vec![
557                    Field::new("first", DataType::Int32, false),
558                    Field::new("second", DataType::Utf8, true),
559                ]),
560                false,
561            )),
562            false,
563        );
564        assert_eq!(
565            from_arrow_data_type(&positional, Nullability::NonNullable)?,
566            DType::map(
567                DType::Primitive(PType::I32, Nullability::NonNullable),
568                DType::Utf8(Nullability::Nullable),
569                false,
570                Nullability::NonNullable,
571            )?
572        );
573
574        Ok(())
575    }
576
577    #[test]
578    fn map_dtype_import_rejects_invalid_arrow_shape() {
579        let invalid_entries = [
580            Field::new_struct(
581                "entries",
582                Fields::from(vec![
583                    Field::new("key", DataType::Int32, false),
584                    Field::new("value", DataType::Utf8, true),
585                ]),
586                true,
587            ),
588            Field::new("entries", DataType::Int32, false),
589            Field::new_struct(
590                "entries",
591                Fields::from(vec![Field::new("key", DataType::Int32, false)]),
592                false,
593            ),
594            Field::new_struct(
595                "entries",
596                Fields::from(vec![
597                    Field::new("key", DataType::Int32, true),
598                    Field::new("value", DataType::Utf8, true),
599                ]),
600                false,
601            ),
602        ];
603
604        for entries in invalid_entries {
605            let data_type = DataType::Map(Arc::new(entries), false);
606            assert!(from_arrow_data_type(&data_type, Nullability::NonNullable).is_err());
607        }
608    }
609
610    // Regression test for https://github.com/vortex-data/vortex/issues/8346: unsupported Arrow
611    // types must return an error instead of panicking with `unimplemented!`.
612    #[rstest]
613    #[case::duration(DataType::Duration(ArrowTimeUnit::Microsecond))]
614    #[case::interval(DataType::Interval(arrow_schema::IntervalUnit::DayTime))]
615    #[case::fixed_size_binary(DataType::FixedSizeBinary(3))]
616    fn test_try_from_arrow_unsupported_type_errors(#[case] data_type: DataType) {
617        let err = from_arrow_data_type(&data_type, Nullability::NonNullable)
618            .expect_err("unsupported Arrow type should not convert")
619            .to_string();
620        assert!(err.contains("not supported"), "unexpected error: {err}");
621
622        // The same unsupported type nested in a field or schema must also error cleanly.
623        let field = Field::new("c0", data_type, true);
624        assert!(from_arrow_field_naive(&field).is_err());
625        let schema = Schema::new(vec![field]);
626        assert!(ArrowSession::default().from_arrow_schema(&schema).is_err());
627    }
628
629    #[test]
630    fn test_try_from_arrow_supported_type_succeeds() -> VortexResult<()> {
631        let dtype = from_arrow_data_type(&DataType::Int32, Nullability::Nullable)?;
632        assert_eq!(dtype, DType::Primitive(PType::I32, Nullability::Nullable));
633        Ok(())
634    }
635}