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
38macro_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
46fn 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 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 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
124pub struct ToArrowConverter {
129 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 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 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 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 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 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 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 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}