1use std::num::NonZeroUsize;
52use std::sync::Arc;
53
54use arrow::array::{ArrayRef, RecordBatch};
55use arrow::datatypes::{Field, Schema as ArrowSchema};
56use polars::frame::DataFrame;
57use polars::prelude::{Column, CompatLevel, Series};
58
59use crate::{Result, fmt};
60
61pub const DEFAULT_MAX_BATCH_ROWS: usize = crate::ingress::column_sender::DEFAULT_MAX_CHUNK_ROWS;
66
67const _: () = assert!(
68 std::mem::size_of::<polars_arrow::ffi::ArrowArray>()
69 == std::mem::size_of::<arrow::ffi::FFI_ArrowArray>(),
70);
71const _: () = assert!(
72 std::mem::size_of::<polars_arrow::ffi::ArrowSchema>()
73 == std::mem::size_of::<arrow::ffi::FFI_ArrowSchema>(),
74);
75const _: () = assert!(
76 std::mem::align_of::<polars_arrow::ffi::ArrowArray>()
77 == std::mem::align_of::<arrow::ffi::FFI_ArrowArray>(),
78);
79const _: () = assert!(
80 std::mem::align_of::<polars_arrow::ffi::ArrowSchema>()
81 == std::mem::align_of::<arrow::ffi::FFI_ArrowSchema>(),
82);
83
84#[inline]
93unsafe fn pa_array_into_rs(pa: polars_arrow::ffi::ArrowArray) -> arrow::ffi::FFI_ArrowArray {
94 unsafe { std::mem::transmute::<polars_arrow::ffi::ArrowArray, arrow::ffi::FFI_ArrowArray>(pa) }
95}
96
97#[inline]
98unsafe fn pa_schema_into_rs(pa: polars_arrow::ffi::ArrowSchema) -> arrow::ffi::FFI_ArrowSchema {
99 unsafe {
100 std::mem::transmute::<polars_arrow::ffi::ArrowSchema, arrow::ffi::FFI_ArrowSchema>(pa)
101 }
102}
103
104pub fn dataframe_to_batches(
115 df: &DataFrame,
116 max_rows: Option<NonZeroUsize>,
117) -> DataFrameBatches<'_> {
118 let max_rows = max_rows.map_or(DEFAULT_MAX_BATCH_ROWS, NonZeroUsize::get);
119 let compat = CompatLevel::newest();
120 let cursors: Vec<ColumnCursor<'_>> = (0..df.width())
121 .map(|i| ColumnCursor::new(df.select_at_idx(i).unwrap(), compat))
122 .collect();
123 DataFrameBatches {
124 max_rows,
125 compat,
126 total_rows: df.height(),
127 rows_emitted: 0,
128 cursors,
129 schema: None,
130 poisoned: false,
131 }
132}
133
134pub struct DataFrameBatches<'a> {
138 max_rows: usize,
139 compat: CompatLevel,
140 total_rows: usize,
141 rows_emitted: usize,
142 cursors: Vec<ColumnCursor<'a>>,
143 schema: Option<Arc<ArrowSchema>>,
144 poisoned: bool,
145}
146
147struct ColumnCursor<'a> {
148 name: String,
149 series: &'a Series,
150 pa_field: polars_arrow::datatypes::Field,
151 chunk_lengths: Vec<usize>,
152 chunk_idx: usize,
153 offset_in_chunk: usize,
154 current: Option<Box<dyn polars_arrow::array::Array>>,
155}
156
157impl<'a> ColumnCursor<'a> {
158 fn new(column: &'a Column, compat: CompatLevel) -> Self {
159 let series = column.as_materialized_series();
160 let pa_field = polars_arrow::datatypes::Field::new(
161 series.name().clone(),
162 series.dtype().to_arrow(compat),
163 true,
164 );
165 Self {
166 name: column.name().as_str().to_string(),
167 series,
168 pa_field,
169 chunk_lengths: series.chunk_lengths().collect(),
170 chunk_idx: 0,
171 offset_in_chunk: 0,
172 current: None,
173 }
174 }
175
176 fn skip_empty_chunks(&mut self) {
177 while self.chunk_idx < self.chunk_lengths.len() && self.chunk_lengths[self.chunk_idx] == 0 {
178 self.chunk_idx += 1;
179 self.offset_in_chunk = 0;
180 self.current = None;
181 }
182 }
183
184 fn remaining_in_chunk(&self) -> usize {
185 if self.chunk_idx >= self.chunk_lengths.len() {
186 return 0;
187 }
188 self.chunk_lengths[self.chunk_idx] - self.offset_in_chunk
189 }
190
191 fn current_chunk(&mut self, compat: CompatLevel) -> &dyn polars_arrow::array::Array {
192 let chunk_idx = self.chunk_idx;
193 let series = self.series;
194 let boxed = self
195 .current
196 .get_or_insert_with(|| series.to_arrow(chunk_idx, compat));
197 &**boxed
198 }
199
200 fn advance(&mut self, n: usize) {
201 self.offset_in_chunk += n;
202 if self.offset_in_chunk >= self.chunk_lengths[self.chunk_idx] {
203 self.chunk_idx += 1;
204 self.offset_in_chunk = 0;
205 self.current = None;
206 }
207 }
208}
209
210impl Iterator for DataFrameBatches<'_> {
211 type Item = Result<RecordBatch>;
212
213 fn next(&mut self) -> Option<Self::Item> {
214 if self.poisoned || self.cursors.is_empty() || self.rows_emitted >= self.total_rows {
215 return None;
216 }
217 for cursor in &mut self.cursors {
218 cursor.skip_empty_chunks();
219 }
220 let mut seg_len = self.max_rows;
221 for cursor in &self.cursors {
222 seg_len = seg_len.min(cursor.remaining_in_chunk());
223 }
224 if seg_len == 0 {
225 if self.rows_emitted < self.total_rows {
226 self.poisoned = true;
227 return Some(Err(fmt!(
228 ArrowIngest,
229 "internal: column chunk lengths disagree ({} of {} rows emitted)",
230 self.rows_emitted,
231 self.total_rows
232 )));
233 }
234 return None;
235 }
236 let compat = self.compat;
237 let need_schema = self.schema.is_none();
238 let mut fields: Vec<Field> = if need_schema {
239 Vec::with_capacity(self.cursors.len())
240 } else {
241 Vec::new()
242 };
243 let mut arrays: Vec<ArrayRef> = Vec::with_capacity(self.cursors.len());
244 for cursor in &mut self.cursors {
245 let offset = cursor.offset_in_chunk;
246 let chunk = cursor.current_chunk(compat);
247 let chunk_dtype = chunk.dtype().clone();
248 let sliced = chunk.sliced(offset, seg_len);
249 if chunk_dtype != cursor.pa_field.dtype {
250 self.poisoned = true;
251 return Some(Err(fmt!(
252 ArrowIngest,
253 "column '{}': per-chunk Arrow dtype {:?} differs from the pinned schema \
254 dtype {:?}; call DataFrame::rechunk() or cast the column to a stable \
255 dtype before ingest",
256 cursor.name,
257 chunk_dtype,
258 cursor.pa_field.dtype
259 )));
260 }
261 let array_data = match ffi_polars_to_arrow_rs(&cursor.pa_field, sliced, &cursor.name) {
262 Ok(d) => d,
263 Err(e) => {
264 self.poisoned = true;
265 return Some(Err(e));
266 }
267 };
268 if need_schema {
269 fields.push(Field::new(
270 cursor.name.clone(),
271 array_data.data_type().clone(),
272 true,
273 ));
274 }
275 arrays.push(arrow::array::make_array(array_data));
276 }
277 let schema = match &self.schema {
278 Some(s) => s.clone(),
279 None => {
280 let s = Arc::new(ArrowSchema::new(fields));
281 self.schema = Some(s.clone());
282 s
283 }
284 };
285 let rb = match RecordBatch::try_new(schema, arrays) {
286 Ok(rb) => rb,
287 Err(e) => {
288 self.poisoned = true;
289 return Some(Err(fmt!(ArrowIngest, "RecordBatch::try_new failed: {}", e)));
290 }
291 };
292 for cursor in &mut self.cursors {
293 cursor.advance(seg_len);
294 }
295 self.rows_emitted += seg_len;
296 Some(Ok(rb))
297 }
298}
299
300const CHECKPOINT_BATCHES: usize = 64;
306
307#[derive(Clone, Copy, Default)]
326pub struct PolarsIngestOptions<'a> {
327 max_rows: Option<NonZeroUsize>,
328 timestamp_column: Option<crate::ingress::ColumnName<'a>>,
329 overrides: &'a [crate::ingress::column_sender::ArrowColumnOverride<'a>],
330 ack_level: Option<crate::ingress::AckLevel>,
331}
332
333impl<'a> PolarsIngestOptions<'a> {
334 #[must_use]
336 pub fn new() -> Self {
337 Self::default()
338 }
339
340 #[must_use]
344 pub fn max_rows(mut self, rows: usize) -> Self {
345 self.max_rows = NonZeroUsize::new(rows);
346 self
347 }
348
349 #[must_use]
353 pub fn timestamp_column(mut self, column: crate::ingress::ColumnName<'a>) -> Self {
354 self.timestamp_column = Some(column);
355 self
356 }
357
358 #[must_use]
364 pub fn overrides(
365 mut self,
366 overrides: &'a [crate::ingress::column_sender::ArrowColumnOverride<'a>],
367 ) -> Self {
368 self.overrides = overrides;
369 self
370 }
371
372 #[must_use]
383 pub fn ack_level(mut self, level: crate::ingress::AckLevel) -> Self {
384 self.ack_level = Some(level);
385 self
386 }
387}
388
389impl crate::db::BorrowedDirectColumnSender<'_> {
390 pub(crate) fn flush_polars_dataframe<'t, T>(
434 &mut self,
435 table: T,
436 df: &DataFrame,
437 options: &PolarsIngestOptions<'_>,
438 ) -> Result<()>
439 where
440 T: TryInto<crate::ingress::TableName<'t>>,
441 crate::Error: From<T::Error>,
442 {
443 let table = table.try_into()?;
444 let mut deadline =
445 std::time::Instant::now().checked_add(self.reconnect_policy().max_duration());
446 let mut committed = 0usize;
449
450 loop {
451 let committed_before = committed;
452 match drive_from_checkpoint(self, table, df, options, &mut committed) {
453 Ok(()) => return Ok(()),
454 Err(err) if err.code() != crate::ErrorCode::FailoverRetry => return Err(err),
455 Err(err) => {
456 if committed > committed_before {
463 deadline = std::time::Instant::now()
464 .checked_add(self.reconnect_policy().max_duration());
465 } else if crate::db::reconnect_deadline_expired(deadline) {
466 return Err(err);
467 }
468 self.reborrow_with_retry(deadline)?;
469 }
470 }
471 }
472 }
473}
474
475impl crate::db::QuestDb {
476 pub fn flush_polars_dataframe<'t, T>(
500 &self,
501 table: T,
502 df: &DataFrame,
503 options: &PolarsIngestOptions<'_>,
504 ) -> Result<()>
505 where
506 T: TryInto<crate::ingress::TableName<'t>>,
507 crate::Error: From<T::Error>,
508 {
509 let mut sender = self.borrow_direct_column_sender()?;
510 sender.flush_polars_dataframe(table, df, options)
511 }
512}
513
514fn drive_from_checkpoint(
523 sender: &mut crate::db::BorrowedDirectColumnSender<'_>,
524 table: crate::ingress::TableName<'_>,
525 df: &DataFrame,
526 options: &PolarsIngestOptions<'_>,
527 committed: &mut usize,
528) -> Result<()> {
529 let ack = options
532 .ack_level
533 .unwrap_or_else(|| sender.default_ack_level());
534
535 let skip = *committed;
536 let mut last_was_checkpoint = false;
537 for (idx, rb) in dataframe_to_batches(df, options.max_rows).enumerate() {
538 if idx < skip {
539 continue;
540 }
541 let rb = rb?;
542 let checkpoint = (idx + 1) % CHECKPOINT_BATCHES == 0;
546 match (options.timestamp_column, checkpoint) {
547 (Some(ts), false) => {
548 sender.flush_arrow_batch_at_column(table, &rb, ts, options.overrides)?
549 }
550 (None, false) => sender.flush_arrow_batch_at_now(table, &rb, options.overrides)?,
551 (Some(ts), true) => sender.flush_arrow_batch_at_column_and_wait(
552 table,
553 &rb,
554 ts,
555 options.overrides,
556 ack,
557 )?,
558 (None, true) => {
559 sender.flush_arrow_batch_at_now_and_wait(table, &rb, options.overrides, ack)?
560 }
561 }
562 if checkpoint {
563 *committed = idx + 1;
566 }
567 last_was_checkpoint = checkpoint;
568 }
569 if !last_was_checkpoint {
573 sender.commit(ack)?;
574 }
575 Ok(())
576}
577
578fn ffi_polars_to_arrow_rs(
579 pa_field: &polars_arrow::datatypes::Field,
580 pa_array_box: Box<dyn polars_arrow::array::Array>,
581 col_name: &str,
582) -> Result<arrow::array::ArrayData> {
583 let pa_schema = polars_arrow::ffi::export_field_to_c(pa_field);
584 let pa_array = polars_arrow::ffi::export_array_to_c(pa_array_box);
585 let rs_schema = unsafe { pa_schema_into_rs(pa_schema) };
586 let rs_array = unsafe { pa_array_into_rs(pa_array) };
587 let array_data = unsafe { arrow::ffi::from_ffi(rs_array, &rs_schema) }
588 .map_err(|e| fmt!(ArrowIngest, "from_ffi('{}'): {}", col_name, e))?;
589 Ok(array_data)
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use arrow::array::Int64Array;
597 use arrow::array::cast::AsArray;
598 use arrow::array::types::Int64Type;
599 use polars::prelude::{IntoColumn, NamedFrom, PlSmallStr, Series};
600
601 const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
602 const HUNDRED: NonZeroUsize = NonZeroUsize::new(100).unwrap();
603 const THOUSAND: NonZeroUsize = NonZeroUsize::new(1000).unwrap();
604
605 fn make_df() -> DataFrame {
606 let i = Series::new(PlSmallStr::from("i"), &[1i64, 2, 3]).into_column();
607 let f = Series::new(PlSmallStr::from("f"), &[1.5f64, 2.5, 3.5]).into_column();
608 let s = Series::new(PlSmallStr::from("s"), &["a", "b", "c"]).into_column();
609 crate::polars_ffi::df_from_columns(vec![i, f, s]).unwrap()
610 }
611
612 fn collect_ok(it: DataFrameBatches<'_>) -> Vec<RecordBatch> {
613 it.map(|rb| rb.expect("conversion failed")).collect()
614 }
615
616 fn one_batch(df: &DataFrame) -> RecordBatch {
617 let mut batches = collect_ok(dataframe_to_batches(df, None));
618 assert_eq!(batches.len(), 1);
619 batches.pop().unwrap()
620 }
621
622 #[test]
623 fn dataframe_to_batches_preserves_columns_and_height() {
624 let df = make_df();
625 let rb = one_batch(&df);
626 assert_eq!(rb.num_columns(), 3);
627 assert_eq!(rb.num_rows(), 3);
628 assert_eq!(rb.schema().field(0).name(), "i");
629 assert_eq!(rb.schema().field(1).name(), "f");
630 assert_eq!(rb.schema().field(2).name(), "s");
631 }
632
633 #[test]
634 fn polars_ffi_layout_round_trip() {
635 let s = Series::new(PlSmallStr::from("x"), &[10i64, 20, 30, 40, 50]);
636 let pa_field = polars_arrow::datatypes::Field::new(
637 s.name().clone(),
638 s.dtype().to_arrow(CompatLevel::newest()),
639 true,
640 );
641 let pa_arr = s.to_arrow(0, CompatLevel::newest());
642 let exported_array = polars_arrow::ffi::export_array_to_c(pa_arr);
643 let exported_schema = polars_arrow::ffi::export_field_to_c(&pa_field);
644
645 let rs_array = unsafe { pa_array_into_rs(exported_array) };
646 let rs_schema = unsafe { pa_schema_into_rs(exported_schema) };
647 let data = unsafe { arrow::ffi::from_ffi(rs_array, &rs_schema) }
648 .expect("from_ffi after polars-arrow → arrow-rs bridge");
649 data.validate_full()
650 .expect("valid polars export passes full validation");
651
652 let arr = arrow::array::make_array(data);
653 let int_arr = arr.as_primitive::<Int64Type>();
654 assert_eq!(int_arr.len(), 5);
655 assert_eq!(int_arr.value(0), 10);
656 assert_eq!(int_arr.value(1), 20);
657 assert_eq!(int_arr.value(2), 30);
658 assert_eq!(int_arr.value(3), 40);
659 assert_eq!(int_arr.value(4), 50);
660 }
661
662 #[cfg(feature = "polars-egress")]
663 #[test]
664 fn dataframe_round_trip_int_values_match() {
665 let df = make_df();
666 let rb = one_batch(&df);
667 let back = crate::egress::arrow::polars::record_batch_to_dataframe(rb).unwrap();
668 let series = back.select_at_idx(0).unwrap().as_materialized_series();
669 let i64s = series.i64().unwrap();
670 assert_eq!(i64s.get(0), Some(1));
671 assert_eq!(i64s.get(1), Some(2));
672 assert_eq!(i64s.get(2), Some(3));
673 }
674
675 #[cfg(feature = "polars-egress")]
676 #[test]
677 fn dataframe_round_trip_string_values_match() {
678 let df = make_df();
679 let rb = one_batch(&df);
680 let back = crate::egress::arrow::polars::record_batch_to_dataframe(rb).unwrap();
681 let series = back.select_at_idx(2).unwrap().as_materialized_series();
682 let s = series.str().unwrap();
683 assert_eq!(s.get(0), Some("a"));
684 assert_eq!(s.get(1), Some("b"));
685 assert_eq!(s.get(2), Some("c"));
686 }
687
688 #[test]
689 fn dataframe_to_batches_yields_capped_slices() {
690 let df = make_df();
691 let batches = collect_ok(dataframe_to_batches(&df, Some(TWO)));
692 assert_eq!(batches.len(), 2);
693 assert_eq!(batches[0].num_rows(), 2);
694 assert_eq!(batches[1].num_rows(), 1);
695 }
696
697 #[test]
698 fn dataframe_to_batches_default_max_rows_when_none() {
699 let df = make_df();
700 let batches = collect_ok(dataframe_to_batches(&df, None));
701 assert_eq!(batches.len(), 1);
702 assert_eq!(batches[0].num_rows(), 3);
703 }
704
705 #[test]
706 fn dataframe_to_batches_single_yield_when_under_max() {
707 let df = make_df();
708 let batches = collect_ok(dataframe_to_batches(&df, Some(HUNDRED)));
709 assert_eq!(batches.len(), 1);
710 assert_eq!(batches[0].num_rows(), 3);
711 }
712
713 #[test]
714 fn dataframe_to_batches_chunk_aligned_is_zero_copy() {
715 let mut left = crate::polars_ffi::df_from_columns(vec![
716 Series::new(PlSmallStr::from("i"), &[10i64, 20]).into_column(),
717 ])
718 .unwrap();
719 let right = crate::polars_ffi::df_from_columns(vec![
720 Series::new(PlSmallStr::from("i"), &[30i64, 40]).into_column(),
721 ])
722 .unwrap();
723 left.vstack_mut(&right).unwrap();
724 assert_eq!(left.select_at_idx(0).unwrap().n_chunks(), 2);
725
726 let polars_chunks: Vec<*const i64> = {
727 let s = left.select_at_idx(0).unwrap().as_materialized_series();
728 (0..s.n_chunks())
729 .map(|i| {
730 let arr = &s.chunks()[i];
731 let prim: &polars_arrow::array::PrimitiveArray<i64> =
732 arr.as_any().downcast_ref().unwrap();
733 prim.values().as_slice().as_ptr()
734 })
735 .collect()
736 };
737
738 let batches = collect_ok(dataframe_to_batches(&left, Some(THOUSAND)));
739 assert_eq!(batches.len(), 2);
740 for (idx, rb) in batches.iter().enumerate() {
741 assert_eq!(rb.num_rows(), 2);
742 let col: &Int64Array = rb.column(0).as_primitive::<Int64Type>();
743 assert_eq!(col.values().as_ptr(), polars_chunks[idx]);
744 }
745 }
746
747 #[test]
748 fn dataframe_to_batches_chunk_aligned_splits_within_chunk() {
749 let mut left = crate::polars_ffi::df_from_columns(vec![
750 Series::new(PlSmallStr::from("i"), &[1i64, 2, 3]).into_column(),
751 ])
752 .unwrap();
753 let right = crate::polars_ffi::df_from_columns(vec![
754 Series::new(PlSmallStr::from("i"), &[4i64, 5, 6]).into_column(),
755 ])
756 .unwrap();
757 left.vstack_mut(&right).unwrap();
758
759 let batches = collect_ok(dataframe_to_batches(&left, Some(TWO)));
760 let lens: Vec<usize> = batches.iter().map(|rb| rb.num_rows()).collect();
761 assert_eq!(lens, vec![2, 1, 2, 1]);
762 }
763
764 #[test]
765 fn dataframe_to_batches_misaligned_chunks_zero_copy() {
766 let a1 = Series::new(PlSmallStr::from("a"), &[1i64, 2]);
767 let a2 = Series::new(PlSmallStr::from("a"), &[3i64, 4]);
768 let b = Series::new(PlSmallStr::from("b"), &[10i64, 20, 30, 40]);
769 let mut left =
770 crate::polars_ffi::df_from_columns(vec![a1.into_column(), b.slice(0, 2).into_column()])
771 .unwrap();
772 let right =
773 crate::polars_ffi::df_from_columns(vec![a2.into_column(), b.slice(2, 2).into_column()])
774 .unwrap();
775 left.vstack_mut(&right).unwrap();
776 left.with_column(b.into_column()).unwrap();
777 assert_ne!(
778 left.select_at_idx(0)
779 .unwrap()
780 .as_materialized_series()
781 .chunk_lengths()
782 .collect::<Vec<_>>(),
783 left.select_at_idx(1)
784 .unwrap()
785 .as_materialized_series()
786 .chunk_lengths()
787 .collect::<Vec<_>>(),
788 );
789
790 let b_chunk_ptr = {
791 let s = left.select_at_idx(1).unwrap().as_materialized_series();
792 let arr = &s.chunks()[0];
793 let prim: &polars_arrow::array::PrimitiveArray<i64> =
794 arr.as_any().downcast_ref().unwrap();
795 prim.values().as_slice().as_ptr()
796 };
797
798 let batches = collect_ok(dataframe_to_batches(&left, Some(THOUSAND)));
799 assert_eq!(batches.len(), 2);
800 let a0: &Int64Array = batches[0].column(0).as_primitive::<Int64Type>();
801 let b0: &Int64Array = batches[0].column(1).as_primitive::<Int64Type>();
802 let a1: &Int64Array = batches[1].column(0).as_primitive::<Int64Type>();
803 let b1: &Int64Array = batches[1].column(1).as_primitive::<Int64Type>();
804 assert_eq!(a0.values().as_ref(), &[1, 2]);
805 assert_eq!(b0.values().as_ref(), &[10, 20]);
806 assert_eq!(a1.values().as_ref(), &[3, 4]);
807 assert_eq!(b1.values().as_ref(), &[30, 40]);
808 assert_eq!(b0.values().as_ptr(), b_chunk_ptr);
809 assert_eq!(b1.values().as_ptr(), unsafe { b_chunk_ptr.add(2) });
810 }
811
812 #[test]
813 fn dataframe_to_batches_scalar_column_materialises_once() {
814 use polars::prelude::Scalar;
815 let values = Series::new(PlSmallStr::from("v"), &[1i64, 2, 3, 4]);
816 let scalar = Column::new_scalar(PlSmallStr::from("k"), Scalar::from(7i64), 4);
817 let df = crate::polars_ffi::df_from_columns(vec![values.into_column(), scalar]).unwrap();
818
819 let batches = collect_ok(dataframe_to_batches(&df, Some(TWO)));
820 assert_eq!(batches.len(), 2);
821 for rb in &batches {
822 assert_eq!(rb.num_rows(), 2);
823 let k: &Int64Array = rb.column(1).as_primitive::<Int64Type>();
824 assert_eq!(k.values().as_ref(), &[7, 7]);
825 }
826
827 let materialised_ptr = {
828 let s = df.select_at_idx(1).unwrap().as_materialized_series();
829 let arr = &s.chunks()[0];
830 let prim: &polars_arrow::array::PrimitiveArray<i64> =
831 arr.as_any().downcast_ref().unwrap();
832 prim.values().as_slice().as_ptr()
833 };
834 let k0: &Int64Array = batches[0].column(1).as_primitive::<Int64Type>();
835 let k1: &Int64Array = batches[1].column(1).as_primitive::<Int64Type>();
836 assert_eq!(k0.values().as_ptr(), materialised_ptr);
837 assert_eq!(k1.values().as_ptr(), unsafe { materialised_ptr.add(2) });
838 }
839
840 #[test]
841 fn polars_categorical_routes_through_dictionary() {
842 use arrow::datatypes::DataType as ArrowDataType;
843 use polars::prelude::{CategoricalPhysical, Categories, DataType as PlDataType};
844
845 let cats = Categories::new(
851 PlSmallStr::from("syms"),
852 PlSmallStr::from("test"),
853 CategoricalPhysical::U32,
854 );
855 let mapping = cats.mapping();
856 let dtype = PlDataType::Categorical(cats, mapping);
857
858 let strings = Series::new(PlSmallStr::from("c"), &["A", "B", "A", "C"]);
859 let cat_series = strings.cast(&dtype).unwrap();
860 assert!(matches!(cat_series.dtype(), PlDataType::Categorical(_, _)));
861
862 let df = crate::polars_ffi::df_from_columns(vec![cat_series.into_column()]).unwrap();
863 let batches = collect_ok(dataframe_to_batches(&df, None));
864 assert_eq!(batches.len(), 1);
865 let rb = &batches[0];
866
867 assert!(
868 matches!(
869 rb.schema().field(0).data_type(),
870 ArrowDataType::Dictionary(_, _)
871 ),
872 "expected Dictionary column, got {:?}",
873 rb.schema().field(0).data_type()
874 );
875 assert_eq!(rb.num_rows(), 4);
876 }
877}