1use 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
44pub(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
54pub(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
69pub(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
90pub(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
177pub(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
191pub(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
204pub(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 0..=38 => DataType::Decimal128(precision, scale),
234 39.. => DataType::Decimal256(precision, scale),
236 }
237 }
238 DType::Utf8(_) => DataType::Utf8View,
239 DType::Binary(_) => DataType::BinaryView,
240 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 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 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 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 #[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 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}