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
37macro_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 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 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
102pub struct ToArrowConverter {
107 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 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 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 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 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 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 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}