1use std::collections::HashMap;
19use std::ffi::{CStr, CString};
20use std::ptr::NonNull;
21use std::str::FromStr;
22use std::sync::Arc;
23
24use arrow::array::{Array, ArrayRef, RecordBatch, RecordBatchReader, new_null_array};
25use arrow::compute::can_cast_types;
26use arrow::error::ArrowError;
27use arrow::ffi::FFI_ArrowSchema;
28use arrow::ffi_stream::FFI_ArrowArrayStream;
29use arrow::pyarrow::FromPyArrow;
30use cstr::cstr;
31use datafusion::arrow::datatypes::{Schema, SchemaRef};
32use datafusion::arrow::pyarrow::{PyArrowType, ToPyArrow};
33use datafusion::arrow::util::pretty;
34use datafusion::catalog::TableProvider;
35use datafusion::common::UnnestOptions;
36use datafusion::config::{CsvOptions, ParquetColumnOptions, ParquetOptions, TableParquetOptions};
37use datafusion::dataframe::{DataFrame, DataFrameWriteOptions};
38use datafusion::error::DataFusionError;
39use datafusion::execution::SendableRecordBatchStream;
40use datafusion::execution::context::TaskContext;
41use datafusion::logical_expr::dml::InsertOp;
42use datafusion::logical_expr::{LogicalPlan, SortExpr};
43use datafusion::parquet::basic::{BrotliLevel, Compression, GzipLevel, ZstdLevel};
44use datafusion::physical_plan::{
45 ExecutionPlan as DFExecutionPlan, collect as df_collect,
46 collect_partitioned as df_collect_partitioned, execute_stream as df_execute_stream,
47 execute_stream_partitioned as df_execute_stream_partitioned,
48};
49use datafusion::prelude::*;
50use datafusion_python_util::{is_ipython_env, spawn_future, wait_for_future};
51use futures::{StreamExt, TryStreamExt};
52use parking_lot::Mutex;
53use pyo3::PyErr;
54use pyo3::exceptions::PyValueError;
55use pyo3::prelude::*;
56use pyo3::pybacked::PyBackedStr;
57use pyo3::types::{PyCapsule, PyList, PyTuple, PyTupleMethods};
58
59use crate::common::data_type::PyScalarValue;
60use crate::errors::{PyDataFusionError, PyDataFusionResult, py_datafusion_err};
61use crate::expr::PyExpr;
62use crate::expr::sort_expr::{PySortExpr, to_sort_expressions};
63use crate::physical_plan::PyExecutionPlan;
64use crate::record_batch::{PyRecordBatchStream, poll_next_batch};
65use crate::sql::logical::PyLogicalPlan;
66use crate::table::{PyTable, TempViewTable};
67
68static ARROW_ARRAY_STREAM_NAME: &CStr = cstr!("arrow_array_stream");
70
71type CachedBatches = Option<(Vec<RecordBatch>, bool)>;
74type SharedCachedBatches = Arc<Mutex<CachedBatches>>;
75
76#[derive(Debug, Clone)]
78pub struct FormatterConfig {
79 pub max_bytes: usize,
81 pub min_rows: usize,
83 pub max_rows: usize,
85}
86
87impl Default for FormatterConfig {
88 fn default() -> Self {
89 Self {
90 max_bytes: 2 * 1024 * 1024, min_rows: 10,
92 max_rows: 10,
93 }
94 }
95}
96
97impl FormatterConfig {
98 pub fn validate(&self) -> Result<(), String> {
104 if self.max_bytes == 0 {
105 return Err("max_bytes must be a positive integer".to_string());
106 }
107
108 if self.min_rows == 0 {
109 return Err("min_rows must be a positive integer".to_string());
110 }
111
112 if self.max_rows == 0 {
113 return Err("max_rows must be a positive integer".to_string());
114 }
115
116 if self.min_rows > self.max_rows {
117 return Err("min_rows must be less than or equal to max_rows".to_string());
118 }
119
120 Ok(())
121 }
122}
123
124struct PythonFormatter<'py> {
126 formatter: Bound<'py, PyAny>,
128 config: FormatterConfig,
130}
131
132fn get_python_formatter_with_config(py: Python) -> PyResult<PythonFormatter> {
134 let formatter = import_python_formatter(py)?;
135 let config = build_formatter_config_from_python(&formatter)?;
136 Ok(PythonFormatter { formatter, config })
137}
138
139fn import_python_formatter(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
141 let formatter_module = py.import("datafusion.dataframe_formatter")?;
142 let get_formatter = formatter_module.getattr("get_formatter")?;
143 get_formatter.call0()
144}
145
146fn get_attr<'a, T>(py_object: &'a Bound<'a, PyAny>, attr_name: &str, default_value: T) -> T
148where
149 T: for<'py> pyo3::FromPyObject<'py, 'py> + Clone,
150{
151 py_object
152 .getattr(attr_name)
153 .and_then(|v| v.extract::<T>().map_err(Into::<PyErr>::into))
154 .unwrap_or_else(|_| default_value.clone())
155}
156
157fn build_formatter_config_from_python(formatter: &Bound<'_, PyAny>) -> PyResult<FormatterConfig> {
159 let default_config = FormatterConfig::default();
160 let max_bytes = get_attr(formatter, "max_memory_bytes", default_config.max_bytes);
161 let min_rows = get_attr(formatter, "min_rows", default_config.min_rows);
162
163 let max_rows = get_attr(formatter, "max_rows", 0usize);
167 let max_rows = if max_rows > 0 {
168 max_rows
170 } else {
171 let repr_rows = get_attr(formatter, "repr_rows", 0usize);
173 if repr_rows > 0 {
174 repr_rows
175 } else {
176 default_config.max_rows
178 }
179 };
180
181 let config = FormatterConfig {
182 max_bytes,
183 min_rows,
184 max_rows,
185 };
186
187 config.validate().map_err(PyValueError::new_err)?;
189 Ok(config)
190}
191
192#[pyclass(
194 from_py_object,
195 frozen,
196 name = "ParquetWriterOptions",
197 module = "datafusion",
198 subclass
199)]
200#[derive(Clone, Default)]
201pub struct PyParquetWriterOptions {
202 options: ParquetOptions,
203}
204
205#[pymethods]
206impl PyParquetWriterOptions {
207 #[new]
208 #[allow(clippy::too_many_arguments)]
209 pub fn new(
210 data_pagesize_limit: usize,
211 write_batch_size: usize,
212 writer_version: &str,
213 skip_arrow_metadata: bool,
214 compression: Option<String>,
215 dictionary_enabled: Option<bool>,
216 dictionary_page_size_limit: usize,
217 statistics_enabled: Option<String>,
218 max_row_group_size: usize,
219 created_by: String,
220 column_index_truncate_length: Option<usize>,
221 statistics_truncate_length: Option<usize>,
222 data_page_row_count_limit: usize,
223 encoding: Option<String>,
224 bloom_filter_on_write: bool,
225 bloom_filter_fpp: Option<f64>,
226 bloom_filter_ndv: Option<u64>,
227 allow_single_file_parallelism: bool,
228 maximum_parallel_row_group_writers: usize,
229 maximum_buffered_record_batches_per_stream: usize,
230 ) -> PyResult<Self> {
231 let writer_version =
232 datafusion::common::parquet_config::DFParquetWriterVersion::from_str(writer_version)
233 .map_err(py_datafusion_err)?;
234 Ok(Self {
235 options: ParquetOptions {
236 data_pagesize_limit,
237 write_batch_size,
238 writer_version,
239 skip_arrow_metadata,
240 compression,
241 dictionary_enabled,
242 dictionary_page_size_limit,
243 statistics_enabled,
244 max_row_group_size,
245 created_by,
246 column_index_truncate_length,
247 statistics_truncate_length,
248 data_page_row_count_limit,
249 encoding,
250 bloom_filter_on_write,
251 bloom_filter_fpp,
252 bloom_filter_ndv,
253 allow_single_file_parallelism,
254 maximum_parallel_row_group_writers,
255 maximum_buffered_record_batches_per_stream,
256 ..Default::default()
257 },
258 })
259 }
260}
261
262#[pyclass(
264 from_py_object,
265 frozen,
266 name = "ParquetColumnOptions",
267 module = "datafusion",
268 subclass
269)]
270#[derive(Clone, Default)]
271pub struct PyParquetColumnOptions {
272 options: ParquetColumnOptions,
273}
274
275#[pymethods]
276impl PyParquetColumnOptions {
277 #[new]
278 pub fn new(
279 bloom_filter_enabled: Option<bool>,
280 encoding: Option<String>,
281 dictionary_enabled: Option<bool>,
282 compression: Option<String>,
283 statistics_enabled: Option<String>,
284 bloom_filter_fpp: Option<f64>,
285 bloom_filter_ndv: Option<u64>,
286 ) -> Self {
287 Self {
288 options: ParquetColumnOptions {
289 bloom_filter_enabled,
290 encoding,
291 dictionary_enabled,
292 compression,
293 statistics_enabled,
294 bloom_filter_fpp,
295 bloom_filter_ndv,
296 },
297 }
298 }
299}
300
301#[pyclass(
305 from_py_object,
306 name = "DataFrame",
307 module = "datafusion",
308 subclass,
309 frozen
310)]
311#[derive(Clone)]
312pub struct PyDataFrame {
313 df: Arc<DataFrame>,
314
315 batches: SharedCachedBatches,
317
318 last_plan: Arc<Mutex<Option<Arc<dyn DFExecutionPlan>>>>,
320}
321
322impl PyDataFrame {
323 pub fn new(df: DataFrame) -> Self {
325 Self {
326 df: Arc::new(df),
327 batches: Arc::new(Mutex::new(None)),
328 last_plan: Arc::new(Mutex::new(None)),
329 }
330 }
331
332 pub(crate) fn inner_df(&self) -> Arc<DataFrame> {
334 Arc::clone(&self.df)
335 }
336
337 fn prepare_repr_string<'py>(
338 &self,
339 py: Python<'py>,
340 as_html: bool,
341 ) -> PyDataFusionResult<String> {
342 let PythonFormatter { formatter, config } = get_python_formatter_with_config(py)?;
344
345 let is_ipython = *is_ipython_env(py);
346
347 let (cached_batches, should_cache) = {
348 let mut cache = self.batches.lock();
349 let should_cache = is_ipython && cache.is_none();
350 let batches = cache.take();
351 (batches, should_cache)
352 };
353
354 let (batches, has_more) = match cached_batches {
355 Some(b) => b,
356 None => wait_for_future(
357 py,
358 collect_record_batches_to_display(self.df.as_ref().clone(), config),
359 )??,
360 };
361
362 if batches.is_empty() {
363 return Ok("No data to display".to_string());
365 }
366
367 let table_uuid = uuid::Uuid::new_v4().to_string();
368
369 let py_batches = batches
371 .iter()
372 .map(|rb| rb.to_pyarrow(py))
373 .collect::<PyResult<Vec<Bound<'py, PyAny>>>>()?;
374
375 let py_schema = self.schema().into_pyobject(py)?;
376
377 let kwargs = pyo3::types::PyDict::new(py);
378 let py_batches_list = PyList::new(py, py_batches.as_slice())?;
379 kwargs.set_item("batches", py_batches_list)?;
380 kwargs.set_item("schema", py_schema)?;
381 kwargs.set_item("has_more", has_more)?;
382 kwargs.set_item("table_uuid", table_uuid)?;
383
384 let method_name = match as_html {
385 true => "format_html",
386 false => "format_str",
387 };
388
389 let html_result = formatter.call_method(method_name, (), Some(&kwargs))?;
390 let html_str: String = html_result.extract()?;
391
392 if should_cache {
393 let mut cache = self.batches.lock();
394 *cache = Some((batches.clone(), has_more));
395 }
396
397 Ok(html_str)
398 }
399
400 fn create_and_cache_plan(
404 &self,
405 py: Python,
406 ) -> PyDataFusionResult<(Arc<dyn DFExecutionPlan>, Arc<TaskContext>)> {
407 let df = self.df.as_ref().clone();
408 let new_plan = wait_for_future(py, df.create_physical_plan())??;
409 *self.last_plan.lock() = Some(Arc::clone(&new_plan));
410 let task_ctx = Arc::new(self.df.as_ref().task_ctx());
411 Ok((new_plan, task_ctx))
412 }
413
414 async fn collect_column_inner(&self, column: &str) -> Result<ArrayRef, DataFusionError> {
415 let batches = self
416 .df
417 .as_ref()
418 .clone()
419 .select_columns(&[column])?
420 .collect()
421 .await?;
422
423 let arrays = batches
424 .iter()
425 .map(|b| b.column(0).as_ref())
426 .collect::<Vec<_>>();
427
428 arrow_select::concat::concat(&arrays).map_err(Into::into)
429 }
430}
431
432struct PartitionedDataFrameStreamReader {
439 streams: Vec<SendableRecordBatchStream>,
440 schema: SchemaRef,
441 projection: Option<SchemaRef>,
442 current: usize,
443}
444
445impl Iterator for PartitionedDataFrameStreamReader {
446 type Item = Result<RecordBatch, ArrowError>;
447
448 fn next(&mut self) -> Option<Self::Item> {
449 while self.current < self.streams.len() {
450 let stream = &mut self.streams[self.current];
451 let fut = poll_next_batch(stream);
452 let result = Python::attach(|py| wait_for_future(py, fut));
453
454 match result {
455 Ok(Ok(Some(batch))) => {
456 let batch = if let Some(ref schema) = self.projection {
457 match record_batch_into_schema(batch, schema.as_ref()) {
458 Ok(b) => b,
459 Err(e) => return Some(Err(e)),
460 }
461 } else {
462 batch
463 };
464 return Some(Ok(batch));
465 }
466 Ok(Ok(None)) => {
467 self.current += 1;
468 continue;
469 }
470 Ok(Err(e)) => {
471 return Some(Err(ArrowError::ExternalError(Box::new(e))));
472 }
473 Err(e) => {
474 return Some(Err(ArrowError::ExternalError(Box::new(e))));
475 }
476 }
477 }
478
479 None
480 }
481}
482
483impl RecordBatchReader for PartitionedDataFrameStreamReader {
484 fn schema(&self) -> SchemaRef {
485 self.schema.clone()
486 }
487}
488
489#[pymethods]
490impl PyDataFrame {
491 fn __getitem__(&self, key: Bound<'_, PyAny>) -> PyDataFusionResult<Self> {
493 if let Ok(key) = key.extract::<PyBackedStr>() {
494 self.select_exprs(vec![key])
496 } else if let Ok(tuple) = key.cast::<PyTuple>() {
497 let keys = tuple
499 .iter()
500 .map(|item| item.extract::<PyBackedStr>())
501 .collect::<PyResult<Vec<PyBackedStr>>>()?;
502 self.select_exprs(keys)
503 } else if let Ok(keys) = key.extract::<Vec<PyBackedStr>>() {
504 self.select_exprs(keys)
506 } else {
507 let message = "DataFrame can only be indexed by string index or indices".to_string();
508 Err(PyDataFusionError::Common(message))
509 }
510 }
511
512 fn __repr__(&self, py: Python) -> PyDataFusionResult<String> {
513 self.prepare_repr_string(py, false)
514 }
515
516 #[staticmethod]
517 #[expect(unused_variables)]
518 fn default_str_repr<'py>(
519 batches: Vec<Bound<'py, PyAny>>,
520 schema: &Bound<'py, PyAny>,
521 has_more: bool,
522 table_uuid: &str,
523 ) -> PyResult<String> {
524 let batches = batches
525 .into_iter()
526 .map(|batch| RecordBatch::from_pyarrow_bound(&batch))
527 .collect::<PyResult<Vec<RecordBatch>>>()?
528 .into_iter()
529 .filter(|batch| batch.num_rows() > 0)
530 .collect::<Vec<_>>();
531
532 if batches.is_empty() {
533 return Ok("No data to display".to_owned());
534 }
535
536 let batches_as_displ =
537 pretty::pretty_format_batches(&batches).map_err(py_datafusion_err)?;
538
539 let additional_str = match has_more {
540 true => "\nData truncated.",
541 false => "",
542 };
543
544 Ok(format!("DataFrame()\n{batches_as_displ}{additional_str}"))
545 }
546
547 fn _repr_html_(&self, py: Python) -> PyDataFusionResult<String> {
548 self.prepare_repr_string(py, true)
549 }
550
551 fn describe(&self, py: Python) -> PyDataFusionResult<Self> {
553 let df = self.df.as_ref().clone();
554 let stat_df = wait_for_future(py, df.describe())??;
555 Ok(Self::new(stat_df))
556 }
557
558 fn schema(&self) -> PyArrowType<Schema> {
560 PyArrowType(self.df.schema().as_arrow().clone())
561 }
562
563 #[allow(clippy::wrong_self_convention)]
569 pub fn into_view(&self, temporary: bool) -> PyDataFusionResult<PyTable> {
570 let table_provider = if temporary {
571 Arc::new(TempViewTable::new(Arc::clone(&self.df))) as Arc<dyn TableProvider>
572 } else {
573 self.df.as_ref().clone().into_view()
577 };
578 Ok(PyTable::from(table_provider))
579 }
580
581 fn alias(&self, alias: &str) -> PyDataFusionResult<Self> {
582 let df = self.df.as_ref().clone().alias(alias)?;
583 Ok(Self::new(df))
584 }
585
586 #[pyo3(signature = (*args))]
587 fn select_exprs(&self, args: Vec<PyBackedStr>) -> PyDataFusionResult<Self> {
588 let args = args.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
589 let df = self.df.as_ref().clone().select_exprs(&args)?;
590 Ok(Self::new(df))
591 }
592
593 #[pyo3(signature = (*args))]
594 fn select(&self, args: Vec<PyExpr>) -> PyDataFusionResult<Self> {
595 let expr: Vec<Expr> = args.into_iter().map(|e| e.into()).collect();
596 let df = self.df.as_ref().clone().select(expr)?;
597 Ok(Self::new(df))
598 }
599
600 #[pyo3(signature = (*args))]
601 fn drop(&self, args: Vec<PyBackedStr>) -> PyDataFusionResult<Self> {
602 let cols = args.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
603 let df = self.df.as_ref().clone().drop_columns(&cols)?;
604 Ok(Self::new(df))
605 }
606
607 #[pyo3(signature = (*exprs))]
609 fn window(&self, exprs: Vec<PyExpr>) -> PyDataFusionResult<Self> {
610 let window_exprs = exprs.into_iter().map(|e| e.into()).collect();
611 let df = self.df.as_ref().clone().window(window_exprs)?;
612 Ok(Self::new(df))
613 }
614
615 fn filter(&self, predicate: PyExpr) -> PyDataFusionResult<Self> {
616 let df = self.df.as_ref().clone().filter(predicate.into())?;
617 Ok(Self::new(df))
618 }
619
620 fn parse_sql_expr(&self, expr: PyBackedStr) -> PyDataFusionResult<PyExpr> {
621 self.df
622 .as_ref()
623 .parse_sql_expr(&expr)
624 .map(PyExpr::from)
625 .map_err(PyDataFusionError::from)
626 }
627
628 fn with_column(&self, name: &str, expr: PyExpr) -> PyDataFusionResult<Self> {
629 let df = self.df.as_ref().clone().with_column(name, expr.into())?;
630 Ok(Self::new(df))
631 }
632
633 fn with_columns(&self, exprs: Vec<PyExpr>) -> PyDataFusionResult<Self> {
634 let mut df = self.df.as_ref().clone();
635 for expr in exprs {
636 let expr: Expr = expr.into();
637 let name = format!("{}", expr.schema_name());
638 df = df.with_column(name.as_str(), expr)?
639 }
640 Ok(Self::new(df))
641 }
642
643 fn with_column_renamed(&self, old_name: &str, new_name: &str) -> PyDataFusionResult<Self> {
646 let df = self
647 .df
648 .as_ref()
649 .clone()
650 .with_column_renamed(old_name, new_name)?;
651 Ok(Self::new(df))
652 }
653
654 fn aggregate(&self, group_by: Vec<PyExpr>, aggs: Vec<PyExpr>) -> PyDataFusionResult<Self> {
655 let group_by = group_by.into_iter().map(|e| e.into()).collect();
656 let aggs = aggs.into_iter().map(|e| e.into()).collect();
657 let df = self.df.as_ref().clone().aggregate(group_by, aggs)?;
658 Ok(Self::new(df))
659 }
660
661 #[pyo3(signature = (*exprs))]
662 fn sort(&self, exprs: Vec<PySortExpr>) -> PyDataFusionResult<Self> {
663 let exprs = to_sort_expressions(exprs);
664 let df = self.df.as_ref().clone().sort(exprs)?;
665 Ok(Self::new(df))
666 }
667
668 #[pyo3(signature = (count, offset=0))]
669 fn limit(&self, count: usize, offset: usize) -> PyDataFusionResult<Self> {
670 let df = self.df.as_ref().clone().limit(offset, Some(count))?;
671 Ok(Self::new(df))
672 }
673
674 fn collect<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyAny>>> {
678 let (plan, task_ctx) = self.create_and_cache_plan(py)?;
679 let batches =
680 wait_for_future(py, df_collect(plan, task_ctx))?.map_err(PyDataFusionError::from)?;
681 batches.into_iter().map(|rb| rb.to_pyarrow(py)).collect()
684 }
685
686 fn cache(&self, py: Python) -> PyDataFusionResult<Self> {
688 let df = wait_for_future(py, self.df.as_ref().clone().cache())??;
689 Ok(Self::new(df))
690 }
691
692 fn collect_partitioned<'py>(&self, py: Python<'py>) -> PyResult<Vec<Vec<Bound<'py, PyAny>>>> {
695 let (plan, task_ctx) = self.create_and_cache_plan(py)?;
696 let batches = wait_for_future(py, df_collect_partitioned(plan, task_ctx))?
697 .map_err(PyDataFusionError::from)?;
698
699 batches
700 .into_iter()
701 .map(|rbs| rbs.into_iter().map(|rb| rb.to_pyarrow(py)).collect())
702 .collect()
703 }
704
705 fn collect_column<'py>(&self, py: Python<'py>, column: &str) -> PyResult<Bound<'py, PyAny>> {
706 wait_for_future(py, self.collect_column_inner(column))?
707 .map_err(PyDataFusionError::from)?
708 .to_data()
709 .to_pyarrow(py)
710 }
711
712 #[pyo3(signature = (num=20))]
714 fn show(&self, py: Python, num: usize) -> PyDataFusionResult<()> {
715 let mut df = self.df.as_ref().clone();
716 df = match self.df.logical_plan() {
717 LogicalPlan::Explain(_) | LogicalPlan::Analyze(_) => {
718 df
721 }
722 _ => df.limit(0, Some(num))?,
723 };
724 print_dataframe(py, df)
725 }
726
727 fn distinct(&self) -> PyDataFusionResult<Self> {
729 let df = self.df.as_ref().clone().distinct()?;
730 Ok(Self::new(df))
731 }
732
733 fn join(
734 &self,
735 right: PyDataFrame,
736 how: &str,
737 left_on: Vec<PyBackedStr>,
738 right_on: Vec<PyBackedStr>,
739 coalesce_keys: bool,
740 ) -> PyDataFusionResult<Self> {
741 let join_type = match how {
742 "inner" => JoinType::Inner,
743 "left" => JoinType::Left,
744 "right" => JoinType::Right,
745 "full" => JoinType::Full,
746 "semi" => JoinType::LeftSemi,
747 "anti" => JoinType::LeftAnti,
748 how => {
749 return Err(PyDataFusionError::Common(format!(
750 "The join type {how} does not exist or is not implemented"
751 )));
752 }
753 };
754
755 let left_keys = left_on.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
756 let right_keys = right_on.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
757
758 let mut df = self.df.as_ref().clone().join(
759 right.df.as_ref().clone(),
760 join_type,
761 &left_keys,
762 &right_keys,
763 None,
764 )?;
765
766 if coalesce_keys {
767 let mutual_keys = left_keys
768 .iter()
769 .zip(right_keys.iter())
770 .filter(|(l, r)| l == r)
771 .map(|(key, _)| *key)
772 .collect::<Vec<_>>();
773
774 let fields_to_coalesce = mutual_keys
775 .iter()
776 .map(|name| {
777 let qualified_fields = df
778 .logical_plan()
779 .schema()
780 .qualified_fields_with_unqualified_name(name);
781 (*name, qualified_fields)
782 })
783 .filter(|(_, fields)| fields.len() == 2)
784 .collect::<Vec<_>>();
785
786 let expr: Vec<Expr> = df
787 .logical_plan()
788 .schema()
789 .fields()
790 .into_iter()
791 .enumerate()
792 .map(|(idx, _)| df.logical_plan().schema().qualified_field(idx))
793 .filter_map(|(qualifier, field)| {
794 if let Some((key_name, qualified_fields)) = fields_to_coalesce
795 .iter()
796 .find(|(_, qf)| qf.contains(&(qualifier, field)))
797 {
798 if (qualifier, field) == qualified_fields[0] {
801 let left_col = Expr::Column(Column::from(qualified_fields[0]));
802 let right_col = Expr::Column(Column::from(qualified_fields[1]));
803 return Some(coalesce(vec![left_col, right_col]).alias(*key_name));
804 }
805 None
806 } else {
807 Some(Expr::Column(Column::from((qualifier, field))))
808 }
809 })
810 .collect();
811 df = df.select(expr)?;
812 }
813
814 Ok(Self::new(df))
815 }
816
817 fn join_on(
818 &self,
819 right: PyDataFrame,
820 on_exprs: Vec<PyExpr>,
821 how: &str,
822 ) -> PyDataFusionResult<Self> {
823 let join_type = match how {
824 "inner" => JoinType::Inner,
825 "left" => JoinType::Left,
826 "right" => JoinType::Right,
827 "full" => JoinType::Full,
828 "semi" => JoinType::LeftSemi,
829 "anti" => JoinType::LeftAnti,
830 how => {
831 return Err(PyDataFusionError::Common(format!(
832 "The join type {how} does not exist or is not implemented"
833 )));
834 }
835 };
836 let exprs: Vec<Expr> = on_exprs.into_iter().map(|e| e.into()).collect();
837
838 let df = self
839 .df
840 .as_ref()
841 .clone()
842 .join_on(right.df.as_ref().clone(), join_type, exprs)?;
843 Ok(Self::new(df))
844 }
845
846 #[pyo3(signature = (verbose=false, analyze=false, format=None))]
848 fn explain(
849 &self,
850 py: Python,
851 verbose: bool,
852 analyze: bool,
853 format: Option<&str>,
854 ) -> PyDataFusionResult<()> {
855 let explain_format = match format {
856 Some(f) => f
857 .parse::<datafusion::common::format::ExplainFormat>()
858 .map_err(|e| {
859 PyDataFusionError::Common(format!("Invalid explain format '{}': {}", f, e))
860 })?,
861 None => datafusion::common::format::ExplainFormat::Indent,
862 };
863 let opts = datafusion::logical_expr::ExplainOption::default()
864 .with_verbose(verbose)
865 .with_analyze(analyze)
866 .with_format(explain_format);
867 let df = self.df.as_ref().clone().explain_with_options(opts)?;
868 print_dataframe(py, df)
869 }
870
871 fn logical_plan(&self) -> PyResult<PyLogicalPlan> {
873 Ok(self.df.as_ref().clone().logical_plan().clone().into())
874 }
875
876 fn optimized_logical_plan(&self) -> PyDataFusionResult<PyLogicalPlan> {
878 Ok(self.df.as_ref().clone().into_optimized_plan()?.into())
879 }
880
881 fn execution_plan(&self, py: Python) -> PyDataFusionResult<PyExecutionPlan> {
886 if let Some(plan) = self.last_plan.lock().as_ref() {
887 return Ok(PyExecutionPlan::new(Arc::clone(plan)));
888 }
889 let plan = wait_for_future(py, self.df.as_ref().clone().create_physical_plan())??;
890 Ok(plan.into())
891 }
892
893 fn repartition(&self, num: usize) -> PyDataFusionResult<Self> {
895 let new_df = self
896 .df
897 .as_ref()
898 .clone()
899 .repartition(Partitioning::RoundRobinBatch(num))?;
900 Ok(Self::new(new_df))
901 }
902
903 #[pyo3(signature = (*args, num))]
905 fn repartition_by_hash(&self, args: Vec<PyExpr>, num: usize) -> PyDataFusionResult<Self> {
906 let expr = args.into_iter().map(|py_expr| py_expr.into()).collect();
907 let new_df = self
908 .df
909 .as_ref()
910 .clone()
911 .repartition(Partitioning::Hash(expr, num))?;
912 Ok(Self::new(new_df))
913 }
914
915 #[pyo3(signature = (py_df, distinct=false))]
918 fn union(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult<Self> {
919 let new_df = if distinct {
920 self.df
921 .as_ref()
922 .clone()
923 .union_distinct(py_df.df.as_ref().clone())?
924 } else {
925 self.df.as_ref().clone().union(py_df.df.as_ref().clone())?
926 };
927
928 Ok(Self::new(new_df))
929 }
930
931 #[pyo3(signature = (columns, preserve_nulls=true, recursions=None))]
932 fn unnest_columns(
933 &self,
934 columns: Vec<String>,
935 preserve_nulls: bool,
936 recursions: Option<Vec<(String, String, usize)>>,
937 ) -> PyDataFusionResult<Self> {
938 let unnest_options = build_unnest_options(preserve_nulls, recursions);
939 let cols = columns.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
940 let df = self
941 .df
942 .as_ref()
943 .clone()
944 .unnest_columns_with_options(&cols, unnest_options)?;
945 Ok(Self::new(df))
946 }
947
948 #[pyo3(signature = (py_df, distinct=false))]
950 fn intersect(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult<Self> {
951 let base = self.df.as_ref().clone();
952 let other = py_df.df.as_ref().clone();
953 let new_df = if distinct {
954 base.intersect_distinct(other)?
955 } else {
956 base.intersect(other)?
957 };
958 Ok(Self::new(new_df))
959 }
960
961 #[pyo3(signature = (py_df, distinct=false))]
963 fn except_all(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult<Self> {
964 let base = self.df.as_ref().clone();
965 let other = py_df.df.as_ref().clone();
966 let new_df = if distinct {
967 base.except_distinct(other)?
968 } else {
969 base.except(other)?
970 };
971 Ok(Self::new(new_df))
972 }
973
974 #[pyo3(signature = (py_df, distinct=false))]
976 fn union_by_name(&self, py_df: PyDataFrame, distinct: bool) -> PyDataFusionResult<Self> {
977 let base = self.df.as_ref().clone();
978 let other = py_df.df.as_ref().clone();
979 let new_df = if distinct {
980 base.union_by_name_distinct(other)?
981 } else {
982 base.union_by_name(other)?
983 };
984 Ok(Self::new(new_df))
985 }
986
987 fn distinct_on(
989 &self,
990 on_expr: Vec<PyExpr>,
991 select_expr: Vec<PyExpr>,
992 sort_expr: Option<Vec<PySortExpr>>,
993 ) -> PyDataFusionResult<Self> {
994 let on_expr = on_expr.into_iter().map(|e| e.into()).collect();
995 let select_expr = select_expr.into_iter().map(|e| e.into()).collect();
996 let sort_expr = sort_expr.map(to_sort_expressions);
997 let df = self
998 .df
999 .as_ref()
1000 .clone()
1001 .distinct_on(on_expr, select_expr, sort_expr)?;
1002 Ok(Self::new(df))
1003 }
1004
1005 fn sort_by(&self, exprs: Vec<PyExpr>) -> PyDataFusionResult<Self> {
1007 let exprs = exprs.into_iter().map(|e| e.into()).collect();
1008 let df = self.df.as_ref().clone().sort_by(exprs)?;
1009 Ok(Self::new(df))
1010 }
1011
1012 fn find_qualified_columns(&self, names: Vec<String>) -> PyDataFusionResult<Vec<PyExpr>> {
1014 let name_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
1015 let qualified = self.df.find_qualified_columns(&name_refs)?;
1016 Ok(qualified
1017 .into_iter()
1018 .map(|q| Expr::Column(Column::from(q)).into())
1019 .collect())
1020 }
1021
1022 fn write_csv(
1024 &self,
1025 py: Python,
1026 path: &str,
1027 with_header: bool,
1028 write_options: Option<PyDataFrameWriteOptions>,
1029 ) -> PyDataFusionResult<()> {
1030 let csv_options = CsvOptions {
1031 has_header: Some(with_header),
1032 ..Default::default()
1033 };
1034 let write_options = write_options
1035 .map(DataFrameWriteOptions::from)
1036 .unwrap_or_default();
1037
1038 wait_for_future(
1039 py,
1040 self.df
1041 .as_ref()
1042 .clone()
1043 .write_csv(path, write_options, Some(csv_options)),
1044 )??;
1045 Ok(())
1046 }
1047
1048 #[pyo3(signature = (
1050 path,
1051 compression="zstd",
1052 compression_level=None,
1053 write_options=None,
1054 ))]
1055 fn write_parquet(
1056 &self,
1057 path: &str,
1058 compression: &str,
1059 compression_level: Option<u32>,
1060 write_options: Option<PyDataFrameWriteOptions>,
1061 py: Python,
1062 ) -> PyDataFusionResult<()> {
1063 fn verify_compression_level(cl: Option<u32>) -> Result<u32, PyErr> {
1064 cl.ok_or(PyValueError::new_err("compression_level is not defined"))
1065 }
1066
1067 let _validated = match compression.to_lowercase().as_str() {
1068 "snappy" => Compression::SNAPPY,
1069 "gzip" => Compression::GZIP(
1070 GzipLevel::try_new(compression_level.unwrap_or(6))
1071 .map_err(|e| PyValueError::new_err(format!("{e}")))?,
1072 ),
1073 "brotli" => Compression::BROTLI(
1074 BrotliLevel::try_new(verify_compression_level(compression_level)?)
1075 .map_err(|e| PyValueError::new_err(format!("{e}")))?,
1076 ),
1077 "zstd" => Compression::ZSTD(
1078 ZstdLevel::try_new(verify_compression_level(compression_level)? as i32)
1079 .map_err(|e| PyValueError::new_err(format!("{e}")))?,
1080 ),
1081 "lzo" => Compression::LZO,
1082 "lz4" => Compression::LZ4,
1083 "lz4_raw" => Compression::LZ4_RAW,
1084 "uncompressed" => Compression::UNCOMPRESSED,
1085 _ => {
1086 return Err(PyDataFusionError::Common(format!(
1087 "Unrecognized compression type {compression}"
1088 )));
1089 }
1090 };
1091
1092 let mut compression_string = compression.to_string();
1093 if let Some(level) = compression_level {
1094 compression_string.push_str(&format!("({level})"));
1095 }
1096
1097 let mut options = TableParquetOptions::default();
1098 options.global.compression = Some(compression_string);
1099 let write_options = write_options
1100 .map(DataFrameWriteOptions::from)
1101 .unwrap_or_default();
1102
1103 wait_for_future(
1104 py,
1105 self.df
1106 .as_ref()
1107 .clone()
1108 .write_parquet(path, write_options, Option::from(options)),
1109 )??;
1110 Ok(())
1111 }
1112
1113 fn write_parquet_with_options(
1115 &self,
1116 path: &str,
1117 options: PyParquetWriterOptions,
1118 column_specific_options: HashMap<String, PyParquetColumnOptions>,
1119 write_options: Option<PyDataFrameWriteOptions>,
1120 py: Python,
1121 ) -> PyDataFusionResult<()> {
1122 let table_options = TableParquetOptions {
1123 global: options.options,
1124 column_specific_options: column_specific_options
1125 .into_iter()
1126 .map(|(k, v)| (k, v.options))
1127 .collect(),
1128 ..Default::default()
1129 };
1130 let write_options = write_options
1131 .map(DataFrameWriteOptions::from)
1132 .unwrap_or_default();
1133 wait_for_future(
1134 py,
1135 self.df.as_ref().clone().write_parquet(
1136 path,
1137 write_options,
1138 Option::from(table_options),
1139 ),
1140 )??;
1141 Ok(())
1142 }
1143
1144 fn write_json(
1146 &self,
1147 path: &str,
1148 py: Python,
1149 write_options: Option<PyDataFrameWriteOptions>,
1150 ) -> PyDataFusionResult<()> {
1151 let write_options = write_options
1152 .map(DataFrameWriteOptions::from)
1153 .unwrap_or_default();
1154 wait_for_future(
1155 py,
1156 self.df
1157 .as_ref()
1158 .clone()
1159 .write_json(path, write_options, None),
1160 )??;
1161 Ok(())
1162 }
1163
1164 fn write_table(
1165 &self,
1166 py: Python,
1167 table_name: &str,
1168 write_options: Option<PyDataFrameWriteOptions>,
1169 ) -> PyDataFusionResult<()> {
1170 let write_options = write_options
1171 .map(DataFrameWriteOptions::from)
1172 .unwrap_or_default();
1173 wait_for_future(
1174 py,
1175 self.df
1176 .as_ref()
1177 .clone()
1178 .write_table(table_name, write_options),
1179 )??;
1180 Ok(())
1181 }
1182
1183 fn to_arrow_table(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1186 let batches = self.collect(py)?.into_pyobject(py)?;
1187
1188 let args = if batches.len()? == 0 {
1191 let schema = self.schema().into_pyobject(py)?;
1192 PyTuple::new(py, &[batches, schema])?
1193 } else {
1194 PyTuple::new(py, &[batches])?
1195 };
1196
1197 let table_class = py.import("pyarrow")?.getattr("Table")?;
1199 let table: Py<PyAny> = table_class.call_method1("from_batches", args)?.into();
1200 Ok(table)
1201 }
1202
1203 #[pyo3(signature = (requested_schema=None))]
1204 fn __arrow_c_stream__<'py>(
1205 &'py self,
1206 py: Python<'py>,
1207 requested_schema: Option<Bound<'py, PyCapsule>>,
1208 ) -> PyDataFusionResult<Bound<'py, PyCapsule>> {
1209 let df = self.df.as_ref().clone();
1210 let streams = spawn_future(py, async move { df.execute_stream_partitioned().await })?;
1211
1212 let mut schema: Schema = self.df.schema().to_owned().as_arrow().clone();
1213 let mut projection: Option<SchemaRef> = None;
1214
1215 if let Some(schema_capsule) = requested_schema {
1216 let data: NonNull<FFI_ArrowSchema> = schema_capsule
1217 .pointer_checked(Some(c"arrow_schema"))?
1218 .cast();
1219 let schema_ptr = unsafe { data.as_ref() };
1220 let desired_schema = Schema::try_from(schema_ptr)?;
1221
1222 schema = project_schema(schema, desired_schema)?;
1223 projection = Some(Arc::new(schema.clone()));
1224 }
1225
1226 let schema_ref = Arc::new(schema.clone());
1227
1228 let reader = PartitionedDataFrameStreamReader {
1229 streams,
1230 schema: schema_ref,
1231 projection,
1232 current: 0,
1233 };
1234 let reader: Box<dyn RecordBatchReader + Send> = Box::new(reader);
1235
1236 let stream = FFI_ArrowArrayStream::new(reader);
1240 let name = CString::new(ARROW_ARRAY_STREAM_NAME.to_bytes()).unwrap();
1241 let capsule = PyCapsule::new(py, stream, Some(name))?;
1242 Ok(capsule)
1243 }
1244
1245 fn execute_stream(&self, py: Python) -> PyDataFusionResult<PyRecordBatchStream> {
1246 let (plan, task_ctx) = self.create_and_cache_plan(py)?;
1247 let stream = spawn_future(py, async move { df_execute_stream(plan, task_ctx) })?;
1248 Ok(PyRecordBatchStream::new(stream))
1249 }
1250
1251 fn execute_stream_partitioned(&self, py: Python) -> PyResult<Vec<PyRecordBatchStream>> {
1252 let (plan, task_ctx) = self.create_and_cache_plan(py)?;
1253 let streams = spawn_future(
1254 py,
1255 async move { df_execute_stream_partitioned(plan, task_ctx) },
1256 )?;
1257 Ok(streams.into_iter().map(PyRecordBatchStream::new).collect())
1258 }
1259
1260 fn to_pandas(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1263 let table = self.to_arrow_table(py)?;
1264
1265 let result = table.call_method0(py, "to_pandas")?;
1267 Ok(result)
1268 }
1269
1270 fn to_pylist(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1273 let table = self.to_arrow_table(py)?;
1274
1275 let result = table.call_method0(py, "to_pylist")?;
1277 Ok(result)
1278 }
1279
1280 fn to_pydict(&self, py: Python) -> PyResult<Py<PyAny>> {
1283 let table = self.to_arrow_table(py)?;
1284
1285 let result = table.call_method0(py, "to_pydict")?;
1287 Ok(result)
1288 }
1289
1290 fn to_polars(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1293 let table = self.to_arrow_table(py)?;
1294 let dataframe = py.import("polars")?.getattr("DataFrame")?;
1295 let args = PyTuple::new(py, &[table])?;
1296 let result: Py<PyAny> = dataframe.call1(args)?.into();
1297 Ok(result)
1298 }
1299
1300 fn count(&self, py: Python) -> PyDataFusionResult<usize> {
1302 Ok(wait_for_future(py, self.df.as_ref().clone().count())??)
1303 }
1304
1305 #[pyo3(signature = (value, columns=None))]
1307 fn fill_null(
1308 &self,
1309 value: Py<PyAny>,
1310 columns: Option<Vec<PyBackedStr>>,
1311 py: Python,
1312 ) -> PyDataFusionResult<Self> {
1313 let scalar_value: PyScalarValue = value.extract(py)?;
1314
1315 let cols = match columns {
1316 Some(col_names) => col_names.iter().map(|c| c.to_string()).collect(),
1317 None => Vec::new(), };
1319
1320 let df = self.df.as_ref().clone().fill_null(scalar_value.0, cols)?;
1321 Ok(Self::new(df))
1322 }
1323}
1324
1325#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
1326#[pyclass(
1327 from_py_object,
1328 frozen,
1329 eq,
1330 eq_int,
1331 name = "InsertOp",
1332 module = "datafusion"
1333)]
1334pub enum PyInsertOp {
1335 APPEND,
1336 REPLACE,
1337 OVERWRITE,
1338}
1339
1340impl From<PyInsertOp> for InsertOp {
1341 fn from(value: PyInsertOp) -> Self {
1342 match value {
1343 PyInsertOp::APPEND => InsertOp::Append,
1344 PyInsertOp::REPLACE => InsertOp::Replace,
1345 PyInsertOp::OVERWRITE => InsertOp::Overwrite,
1346 }
1347 }
1348}
1349
1350#[derive(Debug, Clone)]
1351#[pyclass(
1352 from_py_object,
1353 frozen,
1354 name = "DataFrameWriteOptions",
1355 module = "datafusion"
1356)]
1357pub struct PyDataFrameWriteOptions {
1358 insert_operation: InsertOp,
1359 single_file_output: bool,
1360 partition_by: Vec<String>,
1361 sort_by: Vec<SortExpr>,
1362}
1363
1364impl From<PyDataFrameWriteOptions> for DataFrameWriteOptions {
1365 fn from(value: PyDataFrameWriteOptions) -> Self {
1366 DataFrameWriteOptions::new()
1367 .with_insert_operation(value.insert_operation)
1368 .with_single_file_output(value.single_file_output)
1369 .with_partition_by(value.partition_by)
1370 .with_sort_by(value.sort_by)
1371 }
1372}
1373
1374#[pymethods]
1375impl PyDataFrameWriteOptions {
1376 #[new]
1377 fn new(
1378 insert_operation: Option<PyInsertOp>,
1379 single_file_output: bool,
1380 partition_by: Option<Vec<String>>,
1381 sort_by: Option<Vec<PySortExpr>>,
1382 ) -> Self {
1383 let insert_operation = insert_operation.map(Into::into).unwrap_or(InsertOp::Append);
1384 let sort_by = sort_by
1385 .unwrap_or_default()
1386 .into_iter()
1387 .map(Into::into)
1388 .collect();
1389 Self {
1390 insert_operation,
1391 single_file_output,
1392 partition_by: partition_by.unwrap_or_default(),
1393 sort_by,
1394 }
1395 }
1396}
1397
1398fn build_unnest_options(
1399 preserve_nulls: bool,
1400 recursions: Option<Vec<(String, String, usize)>>,
1401) -> UnnestOptions {
1402 let mut opts = UnnestOptions::default().with_preserve_nulls(preserve_nulls);
1403 if let Some(recs) = recursions {
1404 opts.recursions = recs
1405 .into_iter()
1406 .map(
1407 |(input, output, depth)| datafusion::common::RecursionUnnestOption {
1408 input_column: datafusion::common::Column::from(input.as_str()),
1409 output_column: datafusion::common::Column::from(output.as_str()),
1410 depth,
1411 },
1412 )
1413 .collect();
1414 }
1415 opts
1416}
1417
1418fn print_dataframe(py: Python, df: DataFrame) -> PyDataFusionResult<()> {
1420 let batches = wait_for_future(py, df.collect())??;
1422 let result = if batches.is_empty() {
1423 "DataFrame has no rows".to_string()
1424 } else {
1425 match pretty::pretty_format_batches(&batches) {
1426 Ok(batch) => format!("DataFrame()\n{batch}"),
1427 Err(err) => format!("Error: {:?}", err.to_string()),
1428 }
1429 };
1430
1431 let print = py.import("builtins")?.getattr("print")?;
1434 print.call1((result,))?;
1435 Ok(())
1436}
1437
1438fn project_schema(from_schema: Schema, to_schema: Schema) -> Result<Schema, ArrowError> {
1439 let merged_schema = Schema::try_merge(vec![from_schema, to_schema.clone()])?;
1440
1441 let project_indices: Vec<usize> = to_schema
1442 .fields
1443 .iter()
1444 .map(|field| field.name())
1445 .filter_map(|field_name| merged_schema.index_of(field_name).ok())
1446 .collect();
1447
1448 merged_schema.project(&project_indices)
1449}
1450fn record_batch_into_schema(
1456 record_batch: RecordBatch,
1457 schema: &Schema,
1458) -> Result<RecordBatch, ArrowError> {
1459 let schema = Arc::new(schema.clone());
1460 let base_schema = record_batch.schema();
1461 if base_schema.fields().is_empty() {
1462 return Ok(RecordBatch::new_empty(schema));
1464 }
1465
1466 let array_size = record_batch.column(0).len();
1467 let mut data_arrays = Vec::with_capacity(schema.fields().len());
1468
1469 for field in schema.fields() {
1470 let desired_data_type = field.data_type();
1471 if let Some(original_data) = record_batch.column_by_name(field.name()) {
1472 let original_data_type = original_data.data_type();
1473
1474 if can_cast_types(original_data_type, desired_data_type) {
1475 data_arrays.push(arrow::compute::kernels::cast(
1476 original_data,
1477 desired_data_type,
1478 )?);
1479 } else if field.is_nullable() {
1480 data_arrays.push(new_null_array(desired_data_type, array_size));
1481 } else {
1482 return Err(ArrowError::CastError(format!(
1483 "Attempting to cast to non-nullable and non-castable field {} during schema projection.",
1484 field.name()
1485 )));
1486 }
1487 } else {
1488 if !field.is_nullable() {
1489 return Err(ArrowError::CastError(format!(
1490 "Attempting to set null to non-nullable field {} during schema projection.",
1491 field.name()
1492 )));
1493 }
1494 data_arrays.push(new_null_array(desired_data_type, array_size));
1495 }
1496 }
1497
1498 RecordBatch::try_new(schema, data_arrays)
1499}
1500
1501async fn collect_record_batches_to_display(
1512 df: DataFrame,
1513 config: FormatterConfig,
1514) -> Result<(Vec<RecordBatch>, bool), DataFusionError> {
1515 let FormatterConfig {
1516 max_bytes,
1517 min_rows,
1518 max_rows,
1519 } = config;
1520
1521 let partitioned_stream = df.execute_stream_partitioned().await?;
1522 let mut stream = futures::stream::iter(partitioned_stream).flatten();
1523 let mut size_estimate_so_far = 0;
1524 let mut rows_so_far = 0;
1525 let mut record_batches = Vec::default();
1526 let mut has_more = false;
1527
1528 while (size_estimate_so_far < max_bytes && rows_so_far < max_rows) || rows_so_far < min_rows {
1533 let mut rb = match stream.next().await {
1534 None => {
1535 break;
1536 }
1537 Some(Ok(r)) => r,
1538 Some(Err(e)) => return Err(e),
1539 };
1540
1541 let mut rows_in_rb = rb.num_rows();
1542 if rows_in_rb > 0 {
1543 size_estimate_so_far += rb.get_array_memory_size();
1544
1545 if size_estimate_so_far > max_bytes {
1547 let ratio = max_bytes as f32 / size_estimate_so_far as f32;
1548 let total_rows = rows_in_rb + rows_so_far;
1549
1550 let mut reduced_row_num = (total_rows as f32 * ratio).round() as usize;
1552 if reduced_row_num < min_rows {
1554 reduced_row_num = min_rows.min(total_rows);
1555 }
1556
1557 let limited_rows_this_rb = reduced_row_num - rows_so_far;
1558 if limited_rows_this_rb < rows_in_rb {
1559 rows_in_rb = limited_rows_this_rb;
1560 rb = rb.slice(0, limited_rows_this_rb);
1561 has_more = true;
1562 }
1563 }
1564
1565 if rows_in_rb + rows_so_far > max_rows {
1566 rb = rb.slice(0, max_rows - rows_so_far);
1567 has_more = true;
1568 }
1569
1570 rows_so_far += rb.num_rows();
1571 record_batches.push(rb);
1572 }
1573 }
1574
1575 if record_batches.is_empty() {
1576 return Ok((Vec::default(), false));
1577 }
1578
1579 if !has_more {
1580 has_more = match stream.try_next().await {
1582 Ok(None) => false, Ok(Some(_)) => true,
1584 Err(_) => false, };
1586 }
1587
1588 Ok((record_batches, has_more))
1589}