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