Skip to main content

polars_core/series/arrow_export/
mod.rs

1macro_rules! bail_unhandled_arrow_conversion_dtype_pair {
2    ($input_pl_dtype:expr, $output_arrow_field:expr) => {{
3        return Err(
4            $crate::series::arrow_export::unhandled_arrow_conversion_dtype_pair_err(
5                $input_pl_dtype,
6                $output_arrow_field,
7            ),
8        );
9    }};
10}
11
12#[cfg(feature = "dtype-categorical")]
13pub mod categorical;
14
15use std::borrow::Cow;
16use std::sync::Arc;
17
18use polars_compute::cast::cast_unchecked;
19use polars_compute::rebuild_list::rebuild_list_shallow;
20use polars_error::{PolarsError, PolarsResult, polars_ensure, polars_err};
21
22use crate::prelude::{
23    Array, ArrayRef, ArrowDataType, ArrowField, BinaryViewArray, CompatLevel, DataType, ListArray,
24    PlSmallStr, PrimitiveArray, Series,
25};
26
27fn unhandled_arrow_conversion_dtype_pair_err(
28    input_pl_dtype: &DataType,
29    output_arrow_field: &ArrowField,
30) -> PolarsError {
31    polars_err!(
32        InvalidOperation:
33        "to_arrow() conversion failed: cannot convert \
34        ({input_pl_dtype:?}) to ({output_arrow_field:?})",
35    )
36}
37
38/// Downcasts to a primitive array, boxes it, then sets its dtype.
39macro_rules! primitive_to_boxed_with_logical {
40    ($array:expr, $physical:ty, $logical_arrow_dtype:expr) => {{
41        let arr: &PrimitiveArray<$physical> = $array.as_any().downcast_ref().unwrap();
42        arr.clone().to($logical_arrow_dtype).to_boxed()
43    }};
44}
45
46/// Drop the entries no live row owns and rebase the offsets onto the rest; `None` if the
47/// chunk already spans exactly its child.
48fn normalize_map_entries(arr: &ListArray<i64>) -> Option<ListArray<i64>> {
49    #[cfg(feature = "dtype-map")]
50    if let Some(compacted) = crate::chunked_array::logical::compact_null_rows_chunk(arr) {
51        return Some(compacted);
52    }
53
54    let offsets = arr.offsets();
55    let first = *offsets.first() as usize;
56    let len = offsets.range() as usize;
57    if first == 0 && len == arr.values().len() {
58        return None;
59    }
60    Some(rebuild_list_shallow(
61        arr,
62        arr.dtype().clone(),
63        arr.values().sliced(first, len),
64    ))
65}
66
67fn ensure_no_nulls(array: &dyn Array) -> PolarsResult<()> {
68    polars_ensure!(
69        !array.has_nulls(),
70        SchemaMismatch:
71        "to_arrow() conversion failed: nullable is false but array contained {} NULLs (arrow dtype: {:?})",
72        array.null_count(), array.dtype(),
73    );
74
75    Ok(())
76}
77
78impl Series {
79    /// Export this Series to an arrow array. The dtype of the returned array will be chosen
80    /// according to the provided `compat_level`.
81    pub fn to_arrow(&self, chunk_idx: usize, compat_level: CompatLevel) -> ArrayRef {
82        self.to_arrow_with_field(
83            chunk_idx,
84            Cow::Owned(
85                self.dtype()
86                    .to_arrow_field(self.name().clone(), compat_level),
87            ),
88            true,
89        )
90        .unwrap()
91    }
92
93    /// Export this Series to an arrow array. The dtype of the returned array will match the
94    /// provided arrow field. Returns an error if this Series cannot be exported to the arrow field.
95    pub fn to_arrow_with_field<'a>(
96        &self,
97        chunk_idx: usize,
98        output_arrow_field: Cow<'a, ArrowField>,
99        skip_attach_pl_metadata: bool,
100    ) -> PolarsResult<ArrayRef> {
101        ToArrowConverter {
102            skip_attach_pl_metadata,
103            #[cfg(feature = "dtype-categorical")]
104            categorical_converter: {
105                let mut categorical_converter =
106                    crate::series::arrow_export::categorical::CategoricalToArrowConverter {
107                        converters: Default::default(),
108                        persist_remap: false,
109                    };
110
111                categorical_converter.initialize(self.dtype());
112
113                categorical_converter
114            },
115        }
116        .array_to_arrow(
117            self.chunks().get(chunk_idx).unwrap().as_ref(),
118            self.dtype(),
119            output_arrow_field,
120        )
121    }
122}
123
124/// Low-level converter that exports `ArrayRef`s from Polars Series to arrow arrays.
125///
126/// This can be held to perform repeated categorical exports with persisted indices to ensure
127/// the exported chunks use the same set of indices.
128pub struct ToArrowConverter {
129    /// If the `arrow_field` being passed was generated by `DataType::to_arrow_field`,
130    /// it will already have polars metadata.
131    pub skip_attach_pl_metadata: bool,
132    #[cfg(feature = "dtype-categorical")]
133    pub categorical_converter:
134        crate::series::arrow_export::categorical::CategoricalToArrowConverter,
135}
136
137impl ToArrowConverter {
138    /// Returns an error if `output_arrow_field` was provided and does not match the output data type.
139    pub fn array_to_arrow<'a>(
140        &mut self,
141        array: &dyn Array,
142        dtype: &DataType,
143        arrow_field: Cow<'a, ArrowField>,
144    ) -> PolarsResult<Box<dyn Array>> {
145        let nullable = arrow_field.is_nullable;
146        let out = self.array_to_arrow_impl(array, dtype, arrow_field)?;
147
148        if !nullable {
149            ensure_no_nulls(array)?
150        }
151
152        Ok(out)
153    }
154
155    fn array_to_arrow_impl<'a>(
156        &mut self,
157        array: &dyn Array,
158        polars_dtype: &DataType,
159        arrow_field: Cow<'a, ArrowField>,
160    ) -> PolarsResult<Box<dyn Array>> {
161        // We perform additional steps where necessary. E.g.
162        // * If we are exporting a logical type, set the array dtype to the corresponding arrow logical type.
163        // * Attach field metadata where necessary (e.g. for categorical and extension types).
164        Ok(match (polars_dtype, arrow_field.dtype()) {
165            #[cfg(feature = "dtype-struct")]
166            (DataType::Struct(struct_fields), ArrowDataType::Struct(arrow_struct_fields)) => {
167                use polars_arrow::array::StructArray;
168                let arr: &StructArray = array.as_any().downcast_ref().unwrap();
169
170                polars_ensure!(
171                    arrow_struct_fields.len() == arr.fields().len()
172                    && arrow_struct_fields
173                        .iter()
174                        .zip(arr.fields())
175                        .all(|(l, r)| l.name() == r.name()),
176                    SchemaMismatch:
177                    "to_arrow() conversion failed: struct field names mismatch: {:?} != expected: {:?}",
178                    arrow_field.dtype(), arr.dtype()
179                );
180
181                let mut arrow_dtype = to_owned_dtype(arrow_field);
182
183                let ArrowDataType::Struct(arrow_struct_fields) = &mut arrow_dtype else {
184                    unreachable!()
185                };
186
187                self.attach_pl_field_metadata(
188                    struct_fields
189                        .iter()
190                        .map(|x| x.dtype())
191                        .zip(arrow_struct_fields.iter_mut()),
192                );
193
194                let values: Vec<ArrayRef> = arr
195                    .values()
196                    .iter()
197                    .zip(struct_fields.iter())
198                    .zip(arrow_struct_fields.iter())
199                    .map(|((values, pl_field), arrow_field)| {
200                        self.array_to_arrow(
201                            values.as_ref(),
202                            pl_field.dtype(),
203                            Cow::Borrowed(arrow_field),
204                        )
205                    })
206                    .collect::<PolarsResult<_>>()?;
207
208                let arr =
209                    StructArray::try_new(arrow_dtype, arr.len(), values, arr.validity().cloned())?;
210
211                Box::new(arr)
212            },
213            (DataType::List(item_dtype), ArrowDataType::LargeList(_)) => {
214                let arr: &ListArray<i64> = array.as_any().downcast_ref().unwrap();
215
216                let mut arrow_dtype = to_owned_dtype(arrow_field);
217
218                let ArrowDataType::LargeList(arrow_item_field) = &mut arrow_dtype else {
219                    unreachable!()
220                };
221
222                self.attach_pl_field_metadata(std::iter::once((
223                    item_dtype.as_ref(),
224                    arrow_item_field.as_mut(),
225                )));
226
227                let new_values = self.array_to_arrow(
228                    arr.values().as_ref(),
229                    item_dtype,
230                    Cow::Borrowed(arrow_item_field.as_ref()),
231                )?;
232
233                let arr = ListArray::<i64>::new(
234                    arrow_dtype,
235                    arr.offsets().clone(),
236                    new_values,
237                    arr.validity().cloned(),
238                );
239
240                Box::new(arr)
241            },
242            #[cfg(feature = "dtype-map")]
243            (DataType::Map(_, _), ArrowDataType::Map(_, _)) => {
244                let entries_dtype = polars_dtype.map_entries_dtype().unwrap();
245                self.map_array_to_arrow(array, &entries_dtype, arrow_field)?
246            },
247            (DataType::List(entries_dtype), ArrowDataType::Map(_, _)) => {
248                self.map_array_to_arrow(array, entries_dtype, arrow_field)?
249            },
250            #[cfg(feature = "dtype-map")]
251            (DataType::Map(_, _), ArrowDataType::LargeList(_)) => {
252                let storage_dtype = polars_dtype.map_storage_dtype().unwrap();
253                self.array_to_arrow_impl(array, &storage_dtype, arrow_field)?
254            },
255            #[cfg(feature = "dtype-array")]
256            (DataType::Array(item_dtype, width), ArrowDataType::FixedSizeList(_, arrow_width)) => {
257                use polars_arrow::array::FixedSizeListArray;
258                let arr: &FixedSizeListArray = array.as_any().downcast_ref().unwrap();
259
260                polars_ensure!(
261                    *arrow_width == *width,
262                    SchemaMismatch:
263                    "to_arrow() conversion failed: fixed-size list width mismatch \
264                    ({arrow_width:?} != expected: {width:?})"
265                );
266
267                let mut arrow_dtype = to_owned_dtype(arrow_field);
268
269                let ArrowDataType::FixedSizeList(arrow_item_field, _) = &mut arrow_dtype else {
270                    unreachable!()
271                };
272
273                self.attach_pl_field_metadata(std::iter::once((
274                    item_dtype.as_ref(),
275                    arrow_item_field.as_mut(),
276                )));
277
278                let new_values = self.array_to_arrow(
279                    arr.values().as_ref(),
280                    item_dtype,
281                    Cow::Borrowed(arrow_item_field.as_ref()),
282                )?;
283
284                let arr = FixedSizeListArray::new(
285                    arrow_dtype,
286                    arr.len(),
287                    new_values,
288                    arr.validity().cloned(),
289                );
290
291                Box::new(arr)
292            },
293            #[cfg(feature = "dtype-categorical")]
294            (DataType::Categorical(_, _) | DataType::Enum(_, _), _) => {
295                self.categorical_converter.array_to_arrow(
296                    array,
297                    polars_dtype,
298                    arrow_field.as_ref(),
299                )?
300            },
301            #[cfg(feature = "dtype-date")]
302            (DataType::Date, ArrowDataType::Date32) => {
303                primitive_to_boxed_with_logical!(array, i32, ArrowDataType::Date32)
304            },
305            #[cfg(feature = "dtype-datetime")]
306            (DataType::Datetime(tu, tz), ArrowDataType::Timestamp(atu, atz)) => {
307                use crate::prelude::TimeZone;
308
309                let matching = atu == &tu.to_arrow()
310                    && TimeZone::eq_none_as_utc(
311                        TimeZone::opt_try_new(atz.clone())?.as_ref(),
312                        tz.as_ref(),
313                    );
314
315                if !matching {
316                    bail_unhandled_arrow_conversion_dtype_pair!(polars_dtype, &arrow_field)
317                }
318
319                primitive_to_boxed_with_logical!(array, i64, to_owned_dtype(arrow_field))
320            },
321            #[cfg(feature = "dtype-duration")]
322            (DataType::Duration(tu), ArrowDataType::Duration(atu)) => {
323                let matching = atu == &tu.to_arrow();
324
325                if !matching {
326                    bail_unhandled_arrow_conversion_dtype_pair!(polars_dtype, &arrow_field)
327                }
328
329                primitive_to_boxed_with_logical!(array, i64, to_owned_dtype(arrow_field))
330            },
331            #[cfg(feature = "dtype-time")]
332            (DataType::Time, ArrowDataType::Time64(crate::prelude::ArrowTimeUnit::Nanosecond)) => {
333                primitive_to_boxed_with_logical!(array, i64, to_owned_dtype(arrow_field))
334            },
335            #[cfg(feature = "dtype-time")]
336            (DataType::Time, ArrowDataType::Time64(crate::prelude::ArrowTimeUnit::Microsecond)) => {
337                use polars_compute::cast::time64ns_to_time64us;
338
339                let array: &PrimitiveArray<i64> = array.as_any().downcast_ref().unwrap();
340
341                time64ns_to_time64us(array).boxed()
342            },
343            #[cfg(feature = "dtype-decimal")]
344            (DataType::Decimal(prec, scale), ArrowDataType::Decimal(a_prec, a_scale)) => {
345                let matching = *a_prec == *prec && *a_scale == *scale;
346
347                if !matching {
348                    bail_unhandled_arrow_conversion_dtype_pair!(polars_dtype, &arrow_field)
349                }
350
351                primitive_to_boxed_with_logical!(array, i128, to_owned_dtype(arrow_field))
352            },
353            #[cfg(feature = "object")]
354            (DataType::Object(_), ArrowDataType::FixedSizeBinary(8)) => {
355                use crate::chunked_array::object::builder::object_series_to_arrow_array;
356
357                let out = object_series_to_arrow_array(&unsafe {
358                    Series::from_chunks_and_dtype_unchecked(
359                        PlSmallStr::EMPTY,
360                        vec![array.to_boxed()],
361                        polars_dtype,
362                    )
363                });
364
365                assert_eq!(out.dtype(), &ArrowDataType::FixedSizeBinary(8));
366
367                out
368            },
369            (DataType::String, ArrowDataType::Utf8View) => array.to_boxed(),
370            (DataType::String, ArrowDataType::LargeUtf8) => {
371                cast_unchecked(array, &ArrowDataType::LargeUtf8).unwrap()
372            },
373            (DataType::Binary, ArrowDataType::BinaryView) => array.to_boxed(),
374            (DataType::Binary, ArrowDataType::LargeBinary) => {
375                cast_unchecked(array, &ArrowDataType::LargeBinary).unwrap()
376            },
377            (DataType::Binary, ArrowDataType::FixedSizeBinary(row_width)) => {
378                use polars_compute::cast::binview_to_fixed_binary;
379
380                let array: &BinaryViewArray = array.as_any().downcast_ref().unwrap();
381
382                binview_to_fixed_binary(array, *row_width)?.boxed()
383            },
384            (DataType::Binary, ArrowDataType::Extension(_)) => {
385                let arrow_dtype = to_owned_dtype(arrow_field);
386
387                let ArrowDataType::Extension(ext_type) = &arrow_dtype else {
388                    unreachable!()
389                };
390
391                let storage_field =
392                    ArrowField::new(ext_type.name.clone(), ext_type.inner.clone(), true);
393
394                let mut array =
395                    self.array_to_arrow(array, &DataType::Binary, Cow::Owned(storage_field))?;
396
397                *array.dtype_mut() = arrow_dtype;
398
399                array.to_boxed()
400            },
401            #[cfg(feature = "dtype-extension")]
402            (
403                DataType::Extension(pl_ext_type, storage_dtype),
404                ArrowDataType::Extension(arrow_ext_type),
405            ) => {
406                use polars_arrow::datatypes::ExtensionType;
407
408                let ExtensionType {
409                    name,
410                    inner: _,
411                    metadata,
412                } = arrow_ext_type.as_ref();
413
414                if name != pl_ext_type.name().as_ref() {
415                    bail_unhandled_arrow_conversion_dtype_pair!(polars_dtype, &arrow_field)
416                }
417
418                match (
419                    metadata.as_deref(),
420                    pl_ext_type.serialize_metadata().as_deref(),
421                ) {
422                    (Some("") | None, Some("") | None) => {},
423                    (l, r) => {
424                        if l != r {
425                            bail_unhandled_arrow_conversion_dtype_pair!(polars_dtype, &arrow_field)
426                        }
427                    },
428                };
429
430                let arrow_dtype = to_owned_dtype(arrow_field);
431
432                let ArrowDataType::Extension(arrow_ext_type) = &arrow_dtype else {
433                    unreachable!()
434                };
435
436                let storage_arrow_field = ArrowField::new(
437                    arrow_ext_type.name.clone(),
438                    arrow_ext_type.inner.clone(),
439                    true,
440                );
441
442                let mut arr =
443                    self.array_to_arrow(array, storage_dtype, Cow::Owned(storage_arrow_field))?;
444
445                *arr.dtype_mut() = arrow_dtype;
446
447                arr
448            },
449            (pl_dtype, arrow_dtype) => {
450                if array.dtype() != arrow_dtype {
451                    bail_unhandled_arrow_conversion_dtype_pair!(polars_dtype, &arrow_field)
452                }
453
454                if pl_dtype.is_logical() {
455                    panic!("{pl_dtype:?}");
456                }
457
458                array.to_boxed()
459            },
460        })
461    }
462
463    /// Export a list of map entries as a `MapArray`.
464    fn map_array_to_arrow(
465        &mut self,
466        array: &dyn Array,
467        entries_dtype: &DataType,
468        arrow_field: Cow<'_, ArrowField>,
469    ) -> PolarsResult<Box<dyn Array>> {
470        use polars_arrow::array::MapArray;
471        use polars_arrow::offset::OffsetsBuffer;
472
473        let arr: &ListArray<i64> = array.as_any().downcast_ref().unwrap();
474        // Arrow's MAP entries and keys are non-nullable, and entries that no live row owns
475        // may be null, so normalize before the child is read: those entries are dropped.
476        let normalized = normalize_map_entries(arr);
477        let arr = normalized.as_ref().unwrap_or(arr);
478
479        let mut arrow_dtype = to_owned_dtype(arrow_field);
480
481        let ArrowDataType::Map(arrow_entries_field, _keys_sorted) = &mut arrow_dtype else {
482            unreachable!()
483        };
484
485        self.attach_pl_field_metadata(std::iter::once((
486            entries_dtype,
487            arrow_entries_field.as_mut(),
488        )));
489
490        let entries = self.array_to_arrow(
491            arr.values().as_ref(),
492            entries_dtype,
493            Cow::Borrowed(arrow_entries_field.as_ref()),
494        )?;
495
496        // Arrow's MAP offsets are `i32`, a Polars list's are `i64`.
497        let offsets = OffsetsBuffer::<i32>::try_from(arr.offsets()).map_err(|_| {
498            polars_err!(
499                InvalidOperation:
500                "to_arrow() conversion failed: {} map entries overflow the i32 offsets \
501                of the arrow MAP type",
502                arr.offsets().last(),
503            )
504        })?;
505
506        Ok(Box::new(MapArray::try_new(
507            arrow_dtype,
508            offsets,
509            entries,
510            arr.validity().cloned(),
511        )?))
512    }
513
514    #[inline]
515    fn attach_pl_field_metadata<'a, 'b, I>(&self, iter: I)
516    where
517        I: IntoIterator<Item = (&'a DataType, &'b mut ArrowField)>,
518    {
519        if self.skip_attach_pl_metadata {
520            return;
521        }
522
523        inner(&mut iter.into_iter());
524
525        #[inline(never)]
526        fn inner(iter: &mut dyn Iterator<Item = (&DataType, &mut ArrowField)>) {
527            for (pl_dtype, arrow_field) in iter {
528                match pl_dtype {
529                    #[cfg(feature = "dtype-categorical")]
530                    DataType::Categorical(..) | DataType::Enum(..)
531                        if !matches!(arrow_field.dtype(), ArrowDataType::Dictionary(..)) =>
532                    {
533                        // IPC sink can hit here when it exports only the keys of the categorical.
534                        // In this case we do not want to attach categorical metadata.
535                        continue;
536                    },
537                    _ => {},
538                }
539
540                let mut pl_md = pl_dtype.to_arrow_field_metadata();
541
542                if arrow_field.metadata.is_none() {
543                    arrow_field.metadata = pl_md.take().map(|x| x.into());
544                }
545
546                // Insert polars categorical and enum metadata.
547                if let Some(pl_md) = pl_md
548                    && let Some(md) = arrow_field.metadata.as_mut()
549                {
550                    for (k, v) in pl_md {
551                        if !md.contains_key(&k) {
552                            Arc::make_mut(md).insert(k, v);
553                        }
554                    }
555                }
556            }
557        }
558    }
559}
560
561fn to_owned_dtype(field: Cow<ArrowField>) -> ArrowDataType {
562    match field {
563        Cow::Borrowed(f) => f.dtype().clone(),
564        Cow::Owned(f) => f.dtype,
565    }
566}