1use std::collections::HashMap;
2use std::num::NonZeroUsize;
3
4use arrow::ffi::export_iterator;
5use either::Either;
6use parking_lot::Mutex;
7#[cfg(feature = "pivot")]
8use polars::frame::PivotColumnNaming;
9use polars::io::RowIndex;
10use polars::prelude::iceberg_sink_state::IcebergSinkState;
11use polars::time::*;
12#[cfg(feature = "csv")]
13use polars_buffer::Buffer;
14use polars_core::prelude::*;
15use polars_core::query_result::QueryResult;
16#[cfg(feature = "parquet")]
17use polars_parquet::arrow::write::StatisticsOptions;
18use polars_plan::dsl::ScanSources;
19use polars_plan::plans::{AExpr, HintIR, IR, Sorted};
20use polars_utils::arena::{Arena, Node};
21use polars_utils::python_function::PythonObject;
22use pyo3::exceptions::{PyTypeError, PyValueError};
23use pyo3::prelude::*;
24use pyo3::pybacked::PyBackedStr;
25use pyo3::types::{PyCapsule, PyDict, PyDictMethods, PyList};
26
27use super::{PyLazyFrame, PyOptFlags};
28use crate::error::PyPolarsErr;
29use crate::expr::ToExprs;
30use crate::expr::datatype::PyDataTypeExpr;
31use crate::expr::selector::PySelector;
32use crate::interop::arrow::to_rust::pyarrow_schema_to_rust;
33#[cfg(feature = "json")]
34use crate::io::cloud_options::OptPyCloudOptions;
35use crate::io::scan_options::PyScanOptions;
36use crate::io::sink_options::PySinkOptions;
37use crate::io::sink_output::PyFileSinkDestination;
38use crate::lazyframe::visit::NodeTraverser;
39use crate::prelude::*;
40use crate::utils::{EnterPolarsExt, to_py_err};
41use crate::{PyDataFrame, PyExpr, PyLazyGroupBy};
42
43fn pyobject_to_first_path_and_scan_sources(
44 obj: Py<PyAny>,
45) -> PyResult<(Option<PlRefPath>, ScanSources)> {
46 use crate::file::{PythonScanSourceInput, get_python_scan_source_input};
47 Ok(match get_python_scan_source_input(obj, false)? {
48 PythonScanSourceInput::Path(path) => (
49 Some(path.clone()),
50 ScanSources::Paths(FromIterator::from_iter([path])),
51 ),
52 PythonScanSourceInput::File(file) => (None, ScanSources::Files([file.into()].into())),
53 PythonScanSourceInput::Buffer(buff) => (None, ScanSources::Buffers([buff].into())),
54 })
55}
56
57fn post_opt_callback(
58 lambda: &Py<PyAny>,
59 root: Node,
60 lp_arena: &mut Arena<IR>,
61 expr_arena: &mut Arena<AExpr>,
62 duration_since_start: Option<std::time::Duration>,
63) -> PolarsResult<()> {
64 Python::attach(|py| {
65 let nt = NodeTraverser::new(root, std::mem::take(lp_arena), std::mem::take(expr_arena));
66
67 let arenas = nt.get_arenas();
69
70 lambda
73 .call1(py, (nt, duration_since_start.map(|t| t.as_nanos() as u64)))
74 .map_err(|e| polars_err!(ComputeError: "'cuda' conversion failed: {}", e))?;
75
76 std::mem::swap(lp_arena, &mut *arenas.0.lock().unwrap());
80 std::mem::swap(expr_arena, &mut *arenas.1.lock().unwrap());
81
82 Ok(())
83 })
84}
85
86#[pymethods]
87#[allow(clippy::should_implement_trait)]
88impl PyLazyFrame {
89 #[staticmethod]
90 #[cfg(feature = "json")]
91 #[allow(clippy::too_many_arguments)]
92 #[pyo3(signature = (
93 source, sources, infer_schema_length, schema, schema_overrides, batch_size, n_rows, low_memory, rechunk,
94 row_index, ignore_errors, include_file_paths, cloud_options, credential_provider
95 ))]
96 fn new_from_ndjson(
97 source: Option<Py<PyAny>>,
98 sources: Wrap<ScanSources>,
99 infer_schema_length: Option<usize>,
100 schema: Option<Wrap<Schema>>,
101 schema_overrides: Option<Wrap<Schema>>,
102 batch_size: Option<NonZeroUsize>,
103 n_rows: Option<usize>,
104 low_memory: bool,
105 rechunk: bool,
106 row_index: Option<(String, IdxSize)>,
107 ignore_errors: bool,
108 include_file_paths: Option<String>,
109 cloud_options: OptPyCloudOptions,
110 credential_provider: Option<Py<PyAny>>,
111 ) -> PyResult<Self> {
112 let row_index = row_index.map(|(name, offset)| RowIndex {
113 name: name.into(),
114 offset,
115 });
116
117 let sources = sources.0;
118 let (first_path, sources) = match source {
119 None => (sources.first_path().cloned(), sources),
120 Some(source) => pyobject_to_first_path_and_scan_sources(source)?,
121 };
122
123 let mut r = LazyJsonLineReader::new_with_sources(sources);
124
125 if let Some(first_path) = first_path {
126 let first_path_url = first_path.as_str();
127
128 let cloud_options = cloud_options.extract_opt_cloud_options(
129 CloudScheme::from_path(first_path_url),
130 credential_provider,
131 )?;
132
133 r = r.with_cloud_options(cloud_options);
134 };
135
136 let lf = r
137 .with_infer_schema_length(infer_schema_length.and_then(NonZeroUsize::new))
138 .with_batch_size(batch_size)
139 .with_n_rows(n_rows)
140 .low_memory(low_memory)
141 .with_rechunk(rechunk)
142 .with_schema(schema.map(|schema| Arc::new(schema.0)))
143 .with_schema_overwrite(schema_overrides.map(|x| Arc::new(x.0)))
144 .with_row_index(row_index)
145 .with_ignore_errors(ignore_errors)
146 .with_include_file_paths(include_file_paths.map(|x| x.into()))
147 .finish()
148 .map_err(PyPolarsErr::from)?;
149
150 Ok(lf.into())
151 }
152
153 #[staticmethod]
154 #[cfg(feature = "csv")]
155 #[pyo3(signature = (source, sources, separator, has_header, ignore_errors, skip_rows, skip_lines, n_rows, cache, overwrite_dtype, overwrite_dtype_slice,
156 low_memory, comment_prefix, quote_char, null_values, empty_string_is_null,
157 infer_schema_length, infer_schema_files, new_columns, with_schema_modify, rechunk, skip_rows_after_header,
158 encoding, row_index, try_parse_dates, eol_char, raise_if_empty, truncate_ragged_lines, decimal_comma, glob, schema,
159 cloud_options, credential_provider, include_file_paths, missing_columns
160 )
161 )]
162 fn new_from_csv(
163 source: Option<Py<PyAny>>,
164 sources: Wrap<ScanSources>,
165 separator: &str,
166 has_header: bool,
167 ignore_errors: bool,
168 skip_rows: usize,
169 skip_lines: usize,
170 n_rows: Option<usize>,
171 cache: bool,
172 overwrite_dtype: Option<Vec<(PyBackedStr, Wrap<DataType>)>>,
173 overwrite_dtype_slice: Option<Vec<Wrap<DataType>>>,
174 low_memory: bool,
175 comment_prefix: Option<&str>,
176 quote_char: Option<&str>,
177 null_values: Option<Wrap<NullValues>>,
178 empty_string_is_null: bool,
179 infer_schema_length: Option<usize>,
180 infer_schema_files: NonZeroUsize,
181 new_columns: Option<Wrap<Buffer<PlSmallStr>>>,
182 with_schema_modify: Option<Py<PyAny>>,
183 rechunk: bool,
184 skip_rows_after_header: usize,
185 encoding: Wrap<CsvEncoding>,
186 row_index: Option<(String, IdxSize)>,
187 try_parse_dates: bool,
188 eol_char: &str,
189 raise_if_empty: bool,
190 truncate_ragged_lines: bool,
191 decimal_comma: bool,
192 glob: bool,
193 schema: Option<Wrap<Schema>>,
194 cloud_options: OptPyCloudOptions,
195 credential_provider: Option<Py<PyAny>>,
196 include_file_paths: Option<String>,
197 missing_columns: Option<Wrap<MissingColumnsPolicy>>,
198 ) -> PyResult<Self> {
199 let null_values = null_values.map(|w| w.0);
200 let quote_char = quote_char.and_then(|s| s.as_bytes().first()).copied();
201 let separator = separator
202 .as_bytes()
203 .first()
204 .ok_or_else(|| polars_err!(InvalidOperation: "`separator` cannot be empty"))
205 .copied()
206 .map_err(PyPolarsErr::from)?;
207 let eol_char = eol_char
208 .as_bytes()
209 .first()
210 .ok_or_else(|| polars_err!(InvalidOperation: "`eol_char` cannot be empty"))
211 .copied()
212 .map_err(PyPolarsErr::from)?;
213 let row_index = row_index.map(|(name, offset)| RowIndex {
214 name: name.into(),
215 offset,
216 });
217
218 let overwrite_dtype = overwrite_dtype.map(|overwrite_dtype| {
219 overwrite_dtype
220 .into_iter()
221 .map(|(name, dtype)| Field::new((&*name).into(), dtype.0))
222 .collect::<Schema>()
223 });
224 let overwrite_dtype_slice = overwrite_dtype_slice.map(|overwrite_dtype| {
225 overwrite_dtype
226 .into_iter()
227 .map(|dtype| dtype.0)
228 .collect::<Vec<_>>()
229 });
230
231 let sources = sources.0;
232 let (first_path, sources) = match source {
233 None => (sources.first_path().cloned(), sources),
234 Some(source) => pyobject_to_first_path_and_scan_sources(source)?,
235 };
236
237 let mut r = LazyCsvReader::new_with_sources(sources);
238
239 if let Some(first_path) = first_path {
240 let first_path_url = first_path.as_str();
241 let cloud_options = cloud_options.extract_opt_cloud_options(
242 CloudScheme::from_path(first_path_url),
243 credential_provider,
244 )?;
245 r = r.with_cloud_options(cloud_options);
246 }
247
248 let mut r = r
249 .with_infer_schema_length(infer_schema_length)
250 .with_infer_schema_files(infer_schema_files)
251 .with_separator(separator)
252 .with_has_header(has_header)
253 .with_ignore_errors(ignore_errors)
254 .with_skip_rows(skip_rows)
255 .with_skip_lines(skip_lines)
256 .with_n_rows(n_rows)
257 .with_cache(cache)
258 .with_dtype_overwrite(overwrite_dtype.map(Arc::new))
259 .with_dtype_overwrite_by_position(overwrite_dtype_slice.map(Arc::new))
260 .with_schema(schema.map(|schema| Arc::new(schema.0)))
261 .with_low_memory(low_memory)
262 .with_comment_prefix(comment_prefix.map(|x| x.into()))
263 .with_quote_char(quote_char)
264 .with_eol_char(eol_char)
265 .with_rechunk(rechunk)
266 .with_skip_rows_after_header(skip_rows_after_header)
267 .with_encoding(encoding.0)
268 .with_row_index(row_index)
269 .with_try_parse_dates(try_parse_dates)
270 .with_null_values(null_values)
271 .with_missing_is_null(empty_string_is_null)
272 .with_truncate_ragged_lines(truncate_ragged_lines)
273 .with_decimal_comma(decimal_comma)
274 .with_glob(glob)
275 .with_raise_if_empty(raise_if_empty)
276 .with_include_file_paths(include_file_paths.map(|x| x.into()))
277 .with_missing_columns_policy(missing_columns.map(|x| x.0));
278
279 if let Some(new_columns) = new_columns {
280 r = r.with_column_names_overwrite(new_columns.0);
281 }
282
283 if let Some(lambda) = with_schema_modify {
284 let f = |schema: Schema| {
285 let iter = schema.iter_names().map(|s| s.as_str());
286 Python::attach(|py| {
287 let names = PyList::new(py, iter).unwrap();
288
289 let out = lambda.call1(py, (names,)).expect("python function failed");
290 let new_names = out
291 .extract::<Vec<String>>(py)
292 .expect("python function should return List[str]");
293 polars_ensure!(new_names.len() == schema.len(),
294 ShapeMismatch: "The length of the new names list should be equal to or less than the original column length",
295 );
296 Ok(schema
297 .iter_values()
298 .zip(new_names)
299 .map(|(dtype, name)| Field::new(name.into(), dtype.clone()))
300 .collect())
301 })
302 };
303 r = r.with_schema_modify(f).map_err(PyPolarsErr::from)?
304 }
305
306 Ok(r.finish().map_err(PyPolarsErr::from)?.into())
307 }
308
309 #[cfg(feature = "parquet")]
310 #[staticmethod]
311 #[pyo3(signature = (
312 sources, schema, scan_options, parallel, low_memory, use_statistics
313 ))]
314 fn new_from_parquet(
315 sources: Wrap<ScanSources>,
316 schema: Option<Wrap<Schema>>,
317 scan_options: PyScanOptions,
318 parallel: Wrap<ParallelStrategy>,
319 low_memory: bool,
320 use_statistics: bool,
321 ) -> PyResult<Self> {
322 use crate::utils::to_py_err;
323
324 let parallel = parallel.0;
325
326 let options = ParquetOptions {
327 schema: schema.map(|x| Arc::new(x.0)),
328 parallel,
329 low_memory,
330 use_statistics,
331 };
332
333 let sources = sources.0;
334 let first_path = sources.first_path();
335
336 let unified_scan_args =
337 scan_options.extract_unified_scan_args(first_path.and_then(|x| x.scheme()))?;
338
339 let lf: LazyFrame = DslBuilder::scan_parquet(sources, options, unified_scan_args)
340 .map_err(to_py_err)?
341 .build()
342 .into();
343
344 Ok(lf.into())
345 }
346
347 #[cfg(feature = "ipc")]
348 #[staticmethod]
349 #[pyo3(signature = (sources, record_batch_statistics, scan_options))]
350 fn new_from_ipc(
351 sources: Wrap<ScanSources>,
352 record_batch_statistics: bool,
353 scan_options: PyScanOptions,
354 ) -> PyResult<Self> {
355 let options = IpcScanOptions {
356 record_batch_statistics,
357 checked: Default::default(),
358 };
359
360 let sources = sources.0;
361 let first_path = sources.first_path().cloned();
362
363 let unified_scan_args =
364 scan_options.extract_unified_scan_args(first_path.as_ref().and_then(|x| x.scheme()))?;
365
366 let lf = LazyFrame::scan_ipc_sources(sources, options, unified_scan_args)
367 .map_err(PyPolarsErr::from)?;
368 Ok(lf.into())
369 }
370
371 #[cfg(feature = "scan_lines")]
372 #[staticmethod]
373 #[pyo3(signature = (sources, scan_options, name))]
374 fn new_from_scan_lines(
375 sources: Wrap<ScanSources>,
376 scan_options: PyScanOptions,
377 name: PyBackedStr,
378 ) -> PyResult<Self> {
379 let sources = sources.0;
380 let first_path = sources.first_path();
381
382 let unified_scan_args =
383 scan_options.extract_unified_scan_args(first_path.and_then(|x| x.scheme()))?;
384
385 let dsl: DslPlan = DslBuilder::scan_lines(sources, unified_scan_args, (&*name).into())
386 .map_err(to_py_err)?
387 .build();
388 let lf: LazyFrame = dsl.into();
389
390 Ok(lf.into())
391 }
392
393 #[cfg(feature = "scan_lines")]
394 #[staticmethod]
395 #[pyo3(signature = (sources, scan_options, name))]
396 fn new_from_expand_paths(
397 sources: Wrap<ScanSources>,
398 scan_options: PyScanOptions,
399 name: PyBackedStr,
400 ) -> PyResult<Self> {
401 let sources = sources.0;
402 let first_path = sources.first_path();
403
404 let unified_scan_args =
405 scan_options.extract_unified_scan_args(first_path.and_then(|x| x.scheme()))?;
406
407 let dsl: DslPlan = DslBuilder::expand_paths(sources, unified_scan_args, (&*name).into())
408 .map_err(to_py_err)?
409 .build();
410 let lf: LazyFrame = dsl.into();
411
412 Ok(lf.into())
413 }
414
415 #[staticmethod]
416 #[pyo3(signature = (
417 dataset_object
418 ))]
419 fn new_from_dataset_object(dataset_object: Py<PyAny>) -> PyResult<Self> {
420 let lf =
421 LazyFrame::from(DslBuilder::scan_python_dataset(PythonObject(dataset_object)).build())
422 .into();
423
424 Ok(lf)
425 }
426
427 #[staticmethod]
428 fn scan_from_python_function_arrow_schema(
429 schema: &Bound<'_, PyList>,
430 scan_fn: Py<PyAny>,
431 pyarrow: bool,
432 validate_schema: bool,
433 is_pure: bool,
434 ) -> PyResult<Self> {
435 let schema = Arc::new(pyarrow_schema_to_rust(schema)?);
436
437 Ok(LazyFrame::scan_from_python_function(
438 Either::Right(schema),
439 scan_fn,
440 pyarrow,
441 validate_schema,
442 is_pure,
443 )
444 .into())
445 }
446
447 #[staticmethod]
448 fn scan_from_python_function_pl_schema(
449 schema: Vec<(PyBackedStr, Wrap<DataType>)>,
450 scan_fn: Py<PyAny>,
451 pyarrow: bool,
452 validate_schema: bool,
453 is_pure: bool,
454 ) -> PyResult<Self> {
455 let schema = Arc::new(Schema::from_iter(
456 schema
457 .into_iter()
458 .map(|(name, dt)| Field::new((&*name).into(), dt.0)),
459 ));
460 Ok(LazyFrame::scan_from_python_function(
461 Either::Right(schema),
462 scan_fn,
463 pyarrow,
464 validate_schema,
465 is_pure,
466 )
467 .into())
468 }
469
470 #[staticmethod]
471 fn scan_from_python_function_schema_function(
472 schema_fn: Py<PyAny>,
473 scan_fn: Py<PyAny>,
474 validate_schema: bool,
475 is_pure: bool,
476 ) -> PyResult<Self> {
477 Ok(LazyFrame::scan_from_python_function(
478 Either::Left(schema_fn),
479 scan_fn,
480 false,
481 validate_schema,
482 is_pure,
483 )
484 .into())
485 }
486
487 fn describe_plan(&self, py: Python) -> PyResult<String> {
488 py.enter_polars(|| self.ldf.read().describe_plan())
489 }
490
491 fn describe_optimized_plan(&self, py: Python) -> PyResult<String> {
492 py.enter_polars(|| self.ldf.read().describe_optimized_plan())
493 }
494
495 fn describe_plan_tree(&self, py: Python) -> PyResult<String> {
496 py.enter_polars(|| self.ldf.read().describe_plan_tree())
497 }
498
499 fn describe_optimized_plan_tree(&self, py: Python) -> PyResult<String> {
500 py.enter_polars(|| self.ldf.read().describe_optimized_plan_tree())
501 }
502
503 fn to_dot(&self, py: Python<'_>, optimized: bool) -> PyResult<String> {
504 py.enter_polars(|| self.ldf.read().to_dot(optimized))
505 }
506
507 #[cfg(feature = "streaming")]
508 fn to_dot_streaming_phys(&self, py: Python, optimized: bool) -> PyResult<String> {
509 py.enter_polars(|| self.ldf.read().to_dot_streaming_phys(optimized))
510 }
511
512 fn sort(
513 &self,
514 by_column: &str,
515 descending: bool,
516 nulls_last: bool,
517 maintain_order: bool,
518 multithreaded: bool,
519 ) -> Self {
520 let ldf = self.ldf.read().clone();
521 ldf.sort(
522 [by_column],
523 SortMultipleOptions {
524 descending: vec![descending],
525 nulls_last: vec![nulls_last],
526 multithreaded,
527 maintain_order,
528 limit: None,
529 },
530 )
531 .into()
532 }
533
534 fn sort_by_exprs(
535 &self,
536 by: Vec<PyExpr>,
537 descending: Vec<bool>,
538 nulls_last: Vec<bool>,
539 maintain_order: bool,
540 multithreaded: bool,
541 ) -> Self {
542 let ldf = self.ldf.read().clone();
543 let exprs = by.to_exprs();
544 ldf.sort_by_exprs(
545 exprs,
546 SortMultipleOptions {
547 descending,
548 nulls_last,
549 maintain_order,
550 multithreaded,
551 limit: None,
552 },
553 )
554 .into()
555 }
556
557 fn top_k(&self, k: IdxSize, by: Vec<PyExpr>, reverse: Vec<bool>) -> Self {
558 let ldf = self.ldf.read().clone();
559 let exprs = by.to_exprs();
560 ldf.top_k(
561 k,
562 exprs,
563 SortMultipleOptions::new().with_order_descending_multi(reverse),
564 )
565 .into()
566 }
567
568 fn bottom_k(&self, k: IdxSize, by: Vec<PyExpr>, reverse: Vec<bool>) -> Self {
569 let ldf = self.ldf.read().clone();
570 let exprs = by.to_exprs();
571 ldf.bottom_k(
572 k,
573 exprs,
574 SortMultipleOptions::new().with_order_descending_multi(reverse),
575 )
576 .into()
577 }
578
579 fn cache(&self) -> Self {
580 let ldf = self.ldf.read().clone();
581 ldf.cache().into()
582 }
583
584 #[pyo3(signature = (optflags))]
585 fn with_optimizations(&self, optflags: PyOptFlags) -> Self {
586 let ldf = self.ldf.read().clone();
587 ldf.with_optimizations(optflags.inner.into_inner()).into()
588 }
589
590 #[pyo3(signature = (lambda_post_opt))]
591 fn profile(
592 &self,
593 py: Python<'_>,
594 lambda_post_opt: Option<Py<PyAny>>,
595 ) -> PyResult<(PyDataFrame, PyDataFrame)> {
596 let (df, time_df) = py.enter_polars(|| {
597 let ldf = self.ldf.read().clone();
598 if let Some(lambda) = lambda_post_opt {
599 ldf._profile_post_opt(|root, lp_arena, expr_arena, duration_since_start| {
600 post_opt_callback(&lambda, root, lp_arena, expr_arena, duration_since_start)
601 })
602 } else {
603 ldf.profile()
604 }
605 })?;
606 Ok((df.into(), time_df.into()))
607 }
608
609 #[pyo3(signature = (engine, lambda_post_opt))]
610 fn collect(
611 &self,
612 py: Python<'_>,
613 engine: Wrap<Engine>,
614 lambda_post_opt: Option<Py<PyAny>>,
615 ) -> PyResult<PyDataFrame> {
616 py.enter_polars_df(|| {
617 let ldf = self.ldf.read().clone();
618 if let Some(lambda) = lambda_post_opt {
619 ldf._collect_post_opt(|root, lp_arena, expr_arena, _| {
620 post_opt_callback(&lambda, root, lp_arena, expr_arena, None)
621 })
622 } else {
623 ldf.collect_with_engine(engine.0).map(|r| match r {
624 QueryResult::Single(df) => df,
625 QueryResult::Multiple(_) => DataFrame::empty(),
627 })
628 }
629 })
630 }
631
632 #[cfg(feature = "async")]
633 #[pyo3(signature = (engine, lambda))]
634 fn collect_with_callback(
635 &self,
636 py: Python<'_>,
637 engine: Wrap<Engine>,
638 lambda: Py<PyAny>,
639 ) -> PyResult<()> {
640 py.enter_polars_ok(|| {
641 let ldf = self.ldf.read().clone();
642
643 polars_core::runtime::ASYNC.spawn_blocking(move || {
646 let result = ldf
647 .collect_with_engine(engine.0)
648 .map(|r| match r {
649 QueryResult::Single(df) => df,
650 QueryResult::Multiple(_) => DataFrame::empty(),
652 })
653 .map(PyDataFrame::new)
654 .map_err(PyPolarsErr::from);
655
656 Python::attach(|py| match result {
657 Ok(df) => {
658 lambda.call1(py, (df,)).map_err(|err| err.restore(py)).ok();
659 },
660 Err(err) => {
661 lambda
662 .call1(py, (PyErr::from(err),))
663 .map_err(|err| err.restore(py))
664 .ok();
665 },
666 });
667 });
668 })
669 }
670
671 #[cfg(feature = "async")]
672 fn collect_batches(
673 &self,
674 py: Python<'_>,
675 engine: Wrap<Engine>,
676 maintain_order: bool,
677 chunk_size: Option<NonZeroUsize>,
678 lazy: bool,
679 ) -> PyResult<PyCollectBatches> {
680 py.enter_polars(|| {
681 let ldf = self.ldf.read().clone();
682
683 let collect_batches = ldf
684 .clone()
685 .collect_batches(engine.0, maintain_order, chunk_size, lazy)
686 .map_err(PyPolarsErr::from)?;
687
688 PyResult::Ok(PyCollectBatches {
689 inner: Arc::new(Mutex::new(collect_batches)),
690 ldf,
691 })
692 })
693 }
694
695 #[cfg(feature = "parquet")]
696 #[pyo3(signature = (
697 target, sink_options, compression, compression_level, statistics, row_group_size, data_page_size,
698 metadata, arrow_schema
699 ))]
700 fn sink_parquet(
701 &self,
702 py: Python<'_>,
703 target: PyFileSinkDestination,
704 sink_options: PySinkOptions,
705 compression: &str,
706 compression_level: Option<i32>,
707 statistics: Wrap<StatisticsOptions>,
708 row_group_size: Option<usize>,
709 data_page_size: Option<usize>,
710 metadata: Wrap<Option<KeyValueMetadata>>,
711 arrow_schema: Option<Wrap<ArrowSchema>>,
712 ) -> PyResult<PyLazyFrame> {
713 let compression = parse_parquet_compression(compression, compression_level)?;
714
715 let options = ParquetWriteOptions {
716 compression,
717 statistics: statistics.0,
718 row_group_size,
719 data_page_size,
720 key_value_metadata: metadata.0,
721 arrow_schema: arrow_schema.map(|x| Arc::new(x.0)),
722 compat_level: None,
723 };
724
725 let target = target.extract_file_sink_destination()?;
726 let unified_sink_args = sink_options.extract_unified_sink_args(target.cloud_scheme())?;
727
728 py.enter_polars(|| {
729 self.ldf
730 .read()
731 .clone()
732 .sink(
733 target,
734 FileWriteFormat::Parquet(Arc::new(options)),
735 unified_sink_args,
736 )
737 .into()
738 })
739 .map(Into::into)
740 .map_err(Into::into)
741 }
742
743 #[cfg(feature = "ipc")]
744 #[pyo3(signature = (
745 target, sink_options, compression, compat_level, record_batch_size, record_batch_statistics
746 ))]
747 fn sink_ipc(
748 &self,
749 py: Python<'_>,
750 target: PyFileSinkDestination,
751 sink_options: PySinkOptions,
752 compression: Wrap<Option<IpcCompression>>,
753 compat_level: PyCompatLevel,
754 record_batch_size: Option<usize>,
755 record_batch_statistics: bool,
756 ) -> PyResult<PyLazyFrame> {
757 let options = IpcWriterOptions {
758 compression: compression.0,
759 compat_level: compat_level.0,
760 record_batch_size,
761 record_batch_statistics,
762 };
763
764 let target = target.extract_file_sink_destination()?;
765 let unified_sink_args = sink_options.extract_unified_sink_args(target.cloud_scheme())?;
766
767 py.enter_polars(|| {
768 self.ldf
769 .read()
770 .clone()
771 .sink(target, FileWriteFormat::Ipc(options), unified_sink_args)
772 .into()
773 })
774 .map(Into::into)
775 .map_err(Into::into)
776 }
777
778 #[cfg(feature = "csv")]
779 #[pyo3(signature = (
780 target, sink_options, include_bom, compression, compression_level, check_extension,
781 include_header, separator, line_terminator, quote_char, batch_size, datetime_format,
782 date_format, time_format, float_scientific, float_precision, decimal_comma, null_value,
783 quote_style
784 ))]
785 fn sink_csv(
786 &self,
787 py: Python<'_>,
788 target: PyFileSinkDestination,
789 sink_options: PySinkOptions,
790 include_bom: bool,
791 compression: &str,
792 compression_level: Option<u32>,
793 check_extension: bool,
794 include_header: bool,
795 separator: u8,
796 line_terminator: Wrap<PlSmallStr>,
797 quote_char: u8,
798 batch_size: NonZeroUsize,
799 datetime_format: Option<Wrap<PlSmallStr>>,
800 date_format: Option<Wrap<PlSmallStr>>,
801 time_format: Option<Wrap<PlSmallStr>>,
802 float_scientific: Option<bool>,
803 float_precision: Option<usize>,
804 decimal_comma: bool,
805 null_value: Option<Wrap<PlSmallStr>>,
806 quote_style: Option<Wrap<QuoteStyle>>,
807 ) -> PyResult<PyLazyFrame> {
808 let quote_style = quote_style.map_or(QuoteStyle::default(), |wrap| wrap.0);
809 let null_value = null_value
810 .map(|x| x.0)
811 .unwrap_or(SerializeOptions::default().null);
812
813 let serialize_options = SerializeOptions {
814 date_format: date_format.map(|x| x.0),
815 time_format: time_format.map(|x| x.0),
816 datetime_format: datetime_format.map(|x| x.0),
817 float_scientific,
818 float_precision,
819 decimal_comma,
820 separator,
821 quote_char,
822 null: null_value,
823 line_terminator: line_terminator.0,
824 quote_style,
825 };
826
827 let options = CsvWriterOptions {
828 include_bom,
829 compression: ExternalCompression::try_from(compression, compression_level)
830 .map_err(PyPolarsErr::from)?,
831 check_extension,
832 include_header,
833 batch_size,
834 serialize_options: serialize_options.into(),
835 };
836
837 let target = target.extract_file_sink_destination()?;
838 let unified_sink_args = sink_options.extract_unified_sink_args(target.cloud_scheme())?;
839
840 py.enter_polars(|| {
841 self.ldf
842 .read()
843 .clone()
844 .sink(target, FileWriteFormat::Csv(options), unified_sink_args)
845 .into()
846 })
847 .map(Into::into)
848 .map_err(Into::into)
849 }
850
851 #[allow(clippy::too_many_arguments)]
852 #[cfg(feature = "json")]
853 #[pyo3(signature = (target, compression, compression_level, check_extension, sink_options))]
854 fn sink_ndjson(
855 &self,
856 py: Python<'_>,
857 target: PyFileSinkDestination,
858 compression: &str,
859 compression_level: Option<u32>,
860 check_extension: bool,
861 sink_options: PySinkOptions,
862 ) -> PyResult<PyLazyFrame> {
863 let options = NDJsonWriterOptions {
864 compression: ExternalCompression::try_from(compression, compression_level)
865 .map_err(PyPolarsErr::from)?,
866 check_extension,
867 };
868
869 let target = target.extract_file_sink_destination()?;
870 let unified_sink_args = sink_options.extract_unified_sink_args(target.cloud_scheme())?;
871
872 py.enter_polars(|| {
873 self.ldf
874 .read()
875 .clone()
876 .sink(target, FileWriteFormat::NDJson(options), unified_sink_args)
877 .into()
878 })
879 .map(Into::into)
880 .map_err(Into::into)
881 }
882
883 #[pyo3(signature = (function, maintain_order, chunk_size))]
884 pub fn sink_batches(
885 &self,
886 py: Python<'_>,
887 function: Py<PyAny>,
888 maintain_order: bool,
889 chunk_size: Option<NonZeroUsize>,
890 ) -> PyResult<PyLazyFrame> {
891 let ldf = self.ldf.read().clone();
892 py.enter_polars(|| {
893 ldf.sink_batches(
894 PlanCallback::new_python(PythonObject(function)),
895 maintain_order,
896 chunk_size,
897 )
898 })
899 .map(Into::into)
900 .map_err(Into::into)
901 }
902
903 pub fn sink_iceberg(&self, py: Python<'_>, sink_state_obj: Py<PyAny>) -> PyResult<PyLazyFrame> {
904 let sink_state: IcebergSinkState = sink_state_obj.extract(py)?;
905 let mut ldf = { self.ldf.read().clone() };
906
907 ldf.logical_plan = DslPlan::Sink {
908 input: Arc::new(ldf.logical_plan),
909 payload: SinkType::Iceberg(sink_state),
910 };
911
912 Ok(ldf.into())
913 }
914
915 fn filter(&self, predicate: PyExpr) -> Self {
916 self.ldf.read().clone().filter(predicate.inner).into()
917 }
918
919 fn remove(&self, predicate: PyExpr) -> Self {
920 let ldf = self.ldf.read().clone();
921 ldf.remove(predicate.inner).into()
922 }
923
924 fn select(&self, exprs: Vec<PyExpr>) -> Self {
925 let ldf = self.ldf.read().clone();
926 let exprs = exprs.to_exprs();
927 ldf.select(exprs).into()
928 }
929
930 fn select_seq(&self, exprs: Vec<PyExpr>) -> Self {
931 let ldf = self.ldf.read().clone();
932 let exprs = exprs.to_exprs();
933 ldf.select_seq(exprs).into()
934 }
935
936 fn group_by(&self, by: Vec<PyExpr>, maintain_order: bool) -> PyLazyGroupBy {
937 let ldf = self.ldf.read().clone();
938 let by = by.to_exprs();
939 let lazy_gb = if maintain_order {
940 ldf.group_by_stable(by)
941 } else {
942 ldf.group_by(by)
943 };
944
945 PyLazyGroupBy { lgb: Some(lazy_gb) }
946 }
947
948 fn rolling(
949 &self,
950 index_column: PyExpr,
951 period: &str,
952 offset: &str,
953 closed: Wrap<ClosedWindow>,
954 by: Vec<PyExpr>,
955 ) -> PyResult<PyLazyGroupBy> {
956 let closed_window = closed.0;
957 let ldf = self.ldf.read().clone();
958 let by = by
959 .into_iter()
960 .map(|pyexpr| pyexpr.inner)
961 .collect::<Vec<_>>();
962 let lazy_gb = ldf.rolling(
963 index_column.inner,
964 by,
965 RollingGroupOptions {
966 index_column: "".into(),
967 period: Duration::try_parse(period).map_err(PyPolarsErr::from)?,
968 offset: Duration::try_parse(offset).map_err(PyPolarsErr::from)?,
969 closed_window,
970 },
971 );
972
973 Ok(PyLazyGroupBy { lgb: Some(lazy_gb) })
974 }
975
976 fn group_by_dynamic(
977 &self,
978 index_column: PyExpr,
979 every: &str,
980 period: &str,
981 offset: &str,
982 label: Wrap<Label>,
983 include_boundaries: bool,
984 closed: Wrap<ClosedWindow>,
985 group_by: Vec<PyExpr>,
986 start_by: Wrap<StartBy>,
987 ) -> PyResult<PyLazyGroupBy> {
988 let closed_window = closed.0;
989 let group_by = group_by
990 .into_iter()
991 .map(|pyexpr| pyexpr.inner)
992 .collect::<Vec<_>>();
993 let ldf = self.ldf.read().clone();
994 let lazy_gb = ldf.group_by_dynamic(
995 index_column.inner,
996 group_by,
997 DynamicGroupOptions {
998 every: Duration::try_parse(every).map_err(PyPolarsErr::from)?,
999 period: Duration::try_parse(period).map_err(PyPolarsErr::from)?,
1000 offset: Duration::try_parse(offset).map_err(PyPolarsErr::from)?,
1001 label: label.0,
1002 include_boundaries,
1003 closed_window,
1004 start_by: start_by.0,
1005 ..Default::default()
1006 },
1007 );
1008
1009 Ok(PyLazyGroupBy { lgb: Some(lazy_gb) })
1010 }
1011
1012 fn with_context(&self, contexts: Vec<Self>) -> Self {
1013 let contexts = contexts
1014 .into_iter()
1015 .map(|ldf| ldf.ldf.into_inner())
1016 .collect::<Vec<_>>();
1017 self.ldf.read().clone().with_context(contexts).into()
1018 }
1019
1020 #[cfg(feature = "asof_join")]
1021 #[pyo3(signature = (other, left_on, right_on, left_by, right_by, allow_parallel, force_parallel, suffix, strategy, tolerance, tolerance_str, coalesce, allow_eq, check_sortedness))]
1022 fn join_asof(
1023 &self,
1024 other: Self,
1025 left_on: PyExpr,
1026 right_on: PyExpr,
1027 left_by: Option<Vec<PyBackedStr>>,
1028 right_by: Option<Vec<PyBackedStr>>,
1029 allow_parallel: bool,
1030 force_parallel: bool,
1031 suffix: String,
1032 strategy: Wrap<AsofStrategy>,
1033 tolerance: Option<Wrap<AnyValue<'_>>>,
1034 tolerance_str: Option<String>,
1035 coalesce: bool,
1036 allow_eq: bool,
1037 check_sortedness: bool,
1038 ) -> PyResult<Self> {
1039 let coalesce = if coalesce {
1040 JoinCoalesce::CoalesceColumns
1041 } else {
1042 JoinCoalesce::KeepColumns
1043 };
1044 let ldf = self.ldf.read().clone();
1045 let other = other.ldf.into_inner();
1046 let left_on = left_on.inner;
1047 let right_on = right_on.inner;
1048 Ok(ldf
1049 .join_builder()
1050 .with(other)
1051 .left_on([left_on])
1052 .right_on([right_on])
1053 .allow_parallel(allow_parallel)
1054 .force_parallel(force_parallel)
1055 .coalesce(coalesce)
1056 .how(JoinType::AsOf(Box::new(AsOfOptions {
1057 strategy: strategy.0,
1058 left_by: left_by.map(strings_to_pl_smallstr),
1059 right_by: right_by.map(strings_to_pl_smallstr),
1060 tolerance: tolerance.map(|t| {
1061 let av = t.0.into_static();
1062 let dtype = av.dtype();
1063 Scalar::new(dtype, av)
1064 }),
1065 tolerance_str: tolerance_str.map(|s| s.into()),
1066 allow_eq,
1067 check_sortedness,
1068 })))
1069 .suffix(suffix)
1070 .finish()
1071 .into())
1072 }
1073
1074 #[pyo3(signature = (other, left_on, right_on, allow_parallel, force_parallel, nulls_equal, how, suffix, validate, maintain_order, build_side, coalesce=None))]
1075 fn join(
1076 &self,
1077 other: Self,
1078 left_on: Vec<PyExpr>,
1079 right_on: Vec<PyExpr>,
1080 allow_parallel: bool,
1081 force_parallel: bool,
1082 nulls_equal: bool,
1083 how: Wrap<JoinType>,
1084 suffix: String,
1085 validate: Wrap<JoinValidation>,
1086 maintain_order: Wrap<MaintainOrderJoin>,
1087 build_side: Wrap<Option<JoinBuildSide>>,
1088 coalesce: Option<bool>,
1089 ) -> PyResult<Self> {
1090 let coalesce = match coalesce {
1091 None => JoinCoalesce::JoinSpecific,
1092 Some(true) => JoinCoalesce::CoalesceColumns,
1093 Some(false) => JoinCoalesce::KeepColumns,
1094 };
1095 let ldf = self.ldf.read().clone();
1096 let other = other.ldf.into_inner();
1097 let left_on = left_on
1098 .into_iter()
1099 .map(|pyexpr| pyexpr.inner)
1100 .collect::<Vec<_>>();
1101 let right_on = right_on
1102 .into_iter()
1103 .map(|pyexpr| pyexpr.inner)
1104 .collect::<Vec<_>>();
1105
1106 Ok(ldf
1107 .join_builder()
1108 .with(other)
1109 .left_on(left_on)
1110 .right_on(right_on)
1111 .allow_parallel(allow_parallel)
1112 .force_parallel(force_parallel)
1113 .join_nulls(nulls_equal)
1114 .how(how.0)
1115 .suffix(suffix)
1116 .validate(validate.0)
1117 .coalesce(coalesce)
1118 .maintain_order(maintain_order.0)
1119 .build_side(build_side.0)
1120 .finish()
1121 .into())
1122 }
1123
1124 fn join_where(&self, other: Self, predicates: Vec<PyExpr>, suffix: String) -> PyResult<Self> {
1125 let ldf = self.ldf.read().clone();
1126 let other = other.ldf.into_inner();
1127
1128 let predicates = predicates.to_exprs();
1129
1130 Ok(ldf
1131 .join_builder()
1132 .with(other)
1133 .suffix(suffix)
1134 .join_where(predicates)
1135 .into())
1136 }
1137
1138 fn gather(&self, idxs: Self, null_on_oob: bool) -> Self {
1139 let ldf = self.ldf.read().clone();
1140 let idxs = idxs.ldf.into_inner();
1141 ldf.gather(idxs, null_on_oob).into()
1142 }
1143
1144 fn with_columns(&self, exprs: Vec<PyExpr>) -> Self {
1145 let ldf = self.ldf.read().clone();
1146 ldf.with_columns(exprs.to_exprs()).into()
1147 }
1148
1149 fn with_columns_seq(&self, exprs: Vec<PyExpr>) -> Self {
1150 let ldf = self.ldf.read().clone();
1151 ldf.with_columns_seq(exprs.to_exprs()).into()
1152 }
1153
1154 fn match_to_schema<'py>(
1155 &self,
1156 schema: Wrap<Schema>,
1157 missing_columns: &Bound<'py, PyAny>,
1158 missing_struct_fields: &Bound<'py, PyAny>,
1159 extra_columns: Wrap<ExtraColumnsPolicy>,
1160 extra_struct_fields: &Bound<'py, PyAny>,
1161 integer_cast: &Bound<'py, PyAny>,
1162 float_cast: &Bound<'py, PyAny>,
1163 ) -> PyResult<Self> {
1164 fn parse_missing_columns<'py>(
1165 schema: &Schema,
1166 missing_columns: &Bound<'py, PyAny>,
1167 ) -> PyResult<Vec<MissingColumnsPolicyOrExpr>> {
1168 let mut out = Vec::with_capacity(schema.len());
1169 if let Ok(policy) = missing_columns.extract::<Wrap<MissingColumnsPolicyOrExpr>>() {
1170 out.extend(std::iter::repeat_n(policy.0, schema.len()));
1171 } else if let Ok(dict) = missing_columns.cast::<PyDict>() {
1172 out.extend(std::iter::repeat_n(
1173 MissingColumnsPolicyOrExpr::Raise,
1174 schema.len(),
1175 ));
1176 for (key, value) in dict.iter() {
1177 let key = key.extract::<String>()?;
1178 let value = value.extract::<Wrap<MissingColumnsPolicyOrExpr>>()?;
1179 out[schema.try_index_of(&key).map_err(to_py_err)?] = value.0;
1180 }
1181 } else {
1182 return Err(PyTypeError::new_err("Invalid value for `missing_columns`"));
1183 }
1184 Ok(out)
1185 }
1186 fn parse_missing_struct_fields<'py>(
1187 schema: &Schema,
1188 missing_struct_fields: &Bound<'py, PyAny>,
1189 ) -> PyResult<Vec<MissingColumnsPolicy>> {
1190 let mut out = Vec::with_capacity(schema.len());
1191 if let Ok(policy) = missing_struct_fields.extract::<Wrap<MissingColumnsPolicy>>() {
1192 out.extend(std::iter::repeat_n(policy.0, schema.len()));
1193 } else if let Ok(dict) = missing_struct_fields.cast::<PyDict>() {
1194 out.extend(std::iter::repeat_n(
1195 MissingColumnsPolicy::Raise,
1196 schema.len(),
1197 ));
1198 for (key, value) in dict.iter() {
1199 let key = key.extract::<String>()?;
1200 let value = value.extract::<Wrap<MissingColumnsPolicy>>()?;
1201 out[schema.try_index_of(&key).map_err(to_py_err)?] = value.0;
1202 }
1203 } else {
1204 return Err(PyTypeError::new_err(
1205 "Invalid value for `missing_struct_fields`",
1206 ));
1207 }
1208 Ok(out)
1209 }
1210 fn parse_extra_struct_fields<'py>(
1211 schema: &Schema,
1212 extra_struct_fields: &Bound<'py, PyAny>,
1213 ) -> PyResult<Vec<ExtraColumnsPolicy>> {
1214 let mut out = Vec::with_capacity(schema.len());
1215 if let Ok(policy) = extra_struct_fields.extract::<Wrap<ExtraColumnsPolicy>>() {
1216 out.extend(std::iter::repeat_n(policy.0, schema.len()));
1217 } else if let Ok(dict) = extra_struct_fields.cast::<PyDict>() {
1218 out.extend(std::iter::repeat_n(ExtraColumnsPolicy::Raise, schema.len()));
1219 for (key, value) in dict.iter() {
1220 let key = key.extract::<String>()?;
1221 let value = value.extract::<Wrap<ExtraColumnsPolicy>>()?;
1222 out[schema.try_index_of(&key).map_err(to_py_err)?] = value.0;
1223 }
1224 } else {
1225 return Err(PyTypeError::new_err(
1226 "Invalid value for `extra_struct_fields`",
1227 ));
1228 }
1229 Ok(out)
1230 }
1231 fn parse_cast<'py>(
1232 schema: &Schema,
1233 cast: &Bound<'py, PyAny>,
1234 ) -> PyResult<Vec<UpcastOrForbid>> {
1235 let mut out = Vec::with_capacity(schema.len());
1236 if let Ok(policy) = cast.extract::<Wrap<UpcastOrForbid>>() {
1237 out.extend(std::iter::repeat_n(policy.0, schema.len()));
1238 } else if let Ok(dict) = cast.cast::<PyDict>() {
1239 out.extend(std::iter::repeat_n(UpcastOrForbid::Forbid, schema.len()));
1240 for (key, value) in dict.iter() {
1241 let key = key.extract::<String>()?;
1242 let value = value.extract::<Wrap<UpcastOrForbid>>()?;
1243 out[schema.try_index_of(&key).map_err(to_py_err)?] = value.0;
1244 }
1245 } else {
1246 return Err(PyTypeError::new_err(
1247 "Invalid value for `integer_cast` / `float_cast`",
1248 ));
1249 }
1250 Ok(out)
1251 }
1252
1253 let missing_columns = parse_missing_columns(&schema.0, missing_columns)?;
1254 let missing_struct_fields = parse_missing_struct_fields(&schema.0, missing_struct_fields)?;
1255 let extra_struct_fields = parse_extra_struct_fields(&schema.0, extra_struct_fields)?;
1256 let integer_cast = parse_cast(&schema.0, integer_cast)?;
1257 let float_cast = parse_cast(&schema.0, float_cast)?;
1258
1259 let per_column = (0..schema.0.len())
1260 .map(|i| MatchToSchemaPerColumn {
1261 missing_columns: missing_columns[i].clone(),
1262 missing_struct_fields: missing_struct_fields[i],
1263 extra_struct_fields: extra_struct_fields[i],
1264 integer_cast: integer_cast[i],
1265 float_cast: float_cast[i],
1266 })
1267 .collect();
1268
1269 let ldf = self.ldf.read().clone();
1270 Ok(ldf
1271 .match_to_schema(Arc::new(schema.0), per_column, extra_columns.0)
1272 .into())
1273 }
1274
1275 fn pipe_with_schema(&self, callback: Py<PyAny>) -> Self {
1276 let ldf = self.ldf.read().clone();
1277 let function = PythonObject(callback);
1278 ldf.pipe_with_schema(PlanCallback::new_python(function))
1279 .into()
1280 }
1281
1282 fn rename(&self, existing: Vec<String>, new: Vec<String>, strict: bool) -> Self {
1283 let ldf = self.ldf.read().clone();
1284 ldf.rename(existing, new, strict).into()
1285 }
1286
1287 fn reverse(&self) -> Self {
1288 let ldf = self.ldf.read().clone();
1289 ldf.reverse().into()
1290 }
1291
1292 #[pyo3(signature = (n, fill_value=None))]
1293 fn shift(&self, n: PyExpr, fill_value: Option<PyExpr>) -> Self {
1294 let lf = self.ldf.read().clone();
1295 let out = match fill_value {
1296 Some(v) => lf.shift_and_fill(n.inner, v.inner),
1297 None => lf.shift(n.inner),
1298 };
1299 out.into()
1300 }
1301
1302 fn fill_nan(&self, fill_value: PyExpr) -> Self {
1303 let ldf = self.ldf.read().clone();
1304 ldf.fill_nan(fill_value.inner).into()
1305 }
1306
1307 fn min(&self) -> Self {
1308 let ldf = self.ldf.read().clone();
1309 let out = ldf.min();
1310 out.into()
1311 }
1312
1313 fn max(&self) -> Self {
1314 let ldf = self.ldf.read().clone();
1315 let out = ldf.max();
1316 out.into()
1317 }
1318
1319 fn sum(&self) -> Self {
1320 let ldf = self.ldf.read().clone();
1321 let out = ldf.sum();
1322 out.into()
1323 }
1324
1325 fn mean(&self) -> Self {
1326 let ldf = self.ldf.read().clone();
1327 let out = ldf.mean();
1328 out.into()
1329 }
1330
1331 fn std(&self, ddof: u8) -> Self {
1332 let ldf = self.ldf.read().clone();
1333 let out = ldf.std(ddof);
1334 out.into()
1335 }
1336
1337 fn var(&self, ddof: u8) -> Self {
1338 let ldf = self.ldf.read().clone();
1339 let out = ldf.var(ddof);
1340 out.into()
1341 }
1342
1343 fn median(&self) -> Self {
1344 let ldf = self.ldf.read().clone();
1345 let out = ldf.median();
1346 out.into()
1347 }
1348
1349 fn quantile(&self, quantile: PyExpr, interpolation: Wrap<QuantileMethod>) -> Self {
1350 let ldf = self.ldf.read().clone();
1351 let out = ldf.quantile(quantile.inner, interpolation.0);
1352 out.into()
1353 }
1354
1355 fn explode(&self, subset: PySelector, empty_as_null: bool, keep_nulls: bool) -> Self {
1356 self.ldf
1357 .read()
1358 .clone()
1359 .explode(
1360 subset.inner,
1361 ExplodeOptions {
1362 empty_as_null,
1363 keep_nulls,
1364 },
1365 )
1366 .into()
1367 }
1368
1369 fn null_count(&self) -> Self {
1370 let ldf = self.ldf.read().clone();
1371 ldf.null_count().into()
1372 }
1373
1374 #[pyo3(signature = (maintain_order, subset, keep))]
1375 fn unique(
1376 &self,
1377 maintain_order: bool,
1378 subset: Option<Vec<PyExpr>>,
1379 keep: Wrap<UniqueKeepStrategy>,
1380 ) -> Self {
1381 let ldf = self.ldf.read().clone();
1382 let subset = subset.map(|exprs| exprs.into_iter().map(|e| e.inner).collect());
1383 match maintain_order {
1384 true => ldf.unique_stable_generic(subset, keep.0),
1385 false => ldf.unique_generic(subset, keep.0),
1386 }
1387 .into()
1388 }
1389
1390 fn drop_nans(&self, subset: Option<PySelector>) -> Self {
1391 self.ldf
1392 .read()
1393 .clone()
1394 .drop_nans(subset.map(|e| e.inner))
1395 .into()
1396 }
1397
1398 fn drop_nulls(&self, subset: Option<PySelector>) -> Self {
1399 self.ldf
1400 .read()
1401 .clone()
1402 .drop_nulls(subset.map(|e| e.inner))
1403 .into()
1404 }
1405
1406 #[pyo3(signature = (offset, len=None))]
1407 fn slice(&self, offset: i64, len: Option<IdxSize>) -> Self {
1408 let ldf = self.ldf.read().clone();
1409 ldf.slice(offset, len.unwrap_or(IdxSize::MAX)).into()
1410 }
1411
1412 fn tail(&self, n: IdxSize) -> Self {
1413 let ldf = self.ldf.read().clone();
1414 ldf.tail(n).into()
1415 }
1416
1417 #[cfg(feature = "pivot")]
1418 #[pyo3(signature = (on, on_columns, index, values, agg, maintain_order, separator, column_naming))]
1419 fn pivot(
1420 &self,
1421 on: PySelector,
1422 on_columns: PyDataFrame,
1423 index: PySelector,
1424 values: PySelector,
1425 agg: PyExpr,
1426 maintain_order: bool,
1427 separator: String,
1428 column_naming: Wrap<PivotColumnNaming>,
1429 ) -> Self {
1430 let ldf = self.ldf.read().clone();
1431 ldf.pivot(
1432 on.inner,
1433 Arc::new(on_columns.df.read().clone()),
1434 index.inner,
1435 values.inner,
1436 agg.inner,
1437 maintain_order,
1438 separator.into(),
1439 column_naming.0,
1440 )
1441 .into()
1442 }
1443
1444 #[cfg(feature = "pivot")]
1445 #[pyo3(signature = (on, index, value_name, variable_name))]
1446 fn unpivot(
1447 &self,
1448 on: Option<PySelector>,
1449 index: PySelector,
1450 value_name: Option<String>,
1451 variable_name: Option<String>,
1452 ) -> Self {
1453 let args = UnpivotArgsDSL {
1454 on: on.map(|on| on.inner),
1455 index: index.inner,
1456 value_name: value_name.map(|s| s.into()),
1457 variable_name: variable_name.map(|s| s.into()),
1458 };
1459
1460 let ldf = self.ldf.read().clone();
1461 ldf.unpivot(args).into()
1462 }
1463
1464 #[pyo3(signature = (name, offset=None))]
1465 fn with_row_index(&self, name: &str, offset: Option<IdxSize>) -> Self {
1466 let ldf = self.ldf.read().clone();
1467 ldf.with_row_index(name, offset).into()
1468 }
1469
1470 #[pyo3(signature = (function, predicate_pushdown, projection_pushdown, slice_pushdown, streamable, schema, validate_output))]
1471 fn map_batches(
1472 &self,
1473 function: Py<PyAny>,
1474 predicate_pushdown: bool,
1475 projection_pushdown: bool,
1476 slice_pushdown: bool,
1477 streamable: bool,
1478 schema: Option<Wrap<Schema>>,
1479 validate_output: bool,
1480 ) -> Self {
1481 let mut opt = OptFlags::default();
1482 opt.set(OptFlags::PREDICATE_PUSHDOWN, predicate_pushdown);
1483 opt.set(OptFlags::PROJECTION_PUSHDOWN, projection_pushdown);
1484 opt.set(OptFlags::SLICE_PUSHDOWN, slice_pushdown);
1485 opt.set(OptFlags::STREAMING, streamable);
1486
1487 self.ldf
1488 .read()
1489 .clone()
1490 .map_python(
1491 function.into(),
1492 opt,
1493 schema.map(|s| Arc::new(s.0)),
1494 validate_output,
1495 )
1496 .into()
1497 }
1498
1499 fn drop(&self, columns: PySelector) -> Self {
1500 self.ldf.read().clone().drop(columns.inner).into()
1501 }
1502
1503 fn cast(&self, dtypes: HashMap<PyBackedStr, Wrap<DataType>>, strict: bool) -> Self {
1504 let mut cast_map = PlHashMap::with_capacity(dtypes.len());
1505 cast_map.extend(dtypes.iter().map(|(k, v)| (k.as_ref(), v.0.clone())));
1506 self.ldf.read().clone().cast(cast_map, strict).into()
1507 }
1508
1509 fn cast_all(&self, dtype: PyDataTypeExpr, strict: bool) -> Self {
1510 self.ldf.read().clone().cast_all(dtype.inner, strict).into()
1511 }
1512
1513 fn clone(&self) -> Self {
1514 self.ldf.read().clone().into()
1515 }
1516
1517 fn collect_schema<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1518 let schema = py.enter_polars(|| self.ldf.write().collect_schema())?;
1519
1520 let schema_dict = PyDict::new(py);
1521 schema.iter_fields().for_each(|fld| {
1522 schema_dict
1523 .set_item(fld.name().as_str(), &Wrap(fld.dtype().clone()))
1524 .unwrap()
1525 });
1526 Ok(schema_dict)
1527 }
1528
1529 fn unnest(&self, columns: PySelector, separator: Option<&str>) -> Self {
1530 self.ldf
1531 .read()
1532 .clone()
1533 .unnest(columns.inner, separator.map(PlSmallStr::from_str))
1534 .into()
1535 }
1536
1537 fn count(&self) -> Self {
1538 let ldf = self.ldf.read().clone();
1539 ldf.count().into()
1540 }
1541
1542 #[cfg(feature = "merge_sorted")]
1543 fn merge_sorted(&self, other: Self, key: Vec<String>, maintain_order: bool) -> PyResult<Self> {
1544 let out = self
1545 .ldf
1546 .read()
1547 .clone()
1548 .merge_sorted(other.ldf.into_inner(), key, maintain_order)
1549 .map_err(PyPolarsErr::from)?;
1550 Ok(out.into())
1551 }
1552
1553 fn _node_name(&self) -> &str {
1554 let plan = &self.ldf.read().logical_plan;
1555 plan.into()
1556 }
1557
1558 fn hint_sorted(
1559 &self,
1560 columns: Vec<String>,
1561 descending: Vec<bool>,
1562 nulls_last: Vec<bool>,
1563 ) -> PyResult<Self> {
1564 if columns.len() != descending.len() && descending.len() != 1 {
1565 return Err(PyValueError::new_err(
1566 "`set_sorted` expects the same amount of `columns` as `descending` values.",
1567 ));
1568 }
1569 if columns.len() != nulls_last.len() && nulls_last.len() != 1 {
1570 return Err(PyValueError::new_err(
1571 "`set_sorted` expects the same amount of `columns` as `nulls_last` values.",
1572 ));
1573 }
1574
1575 let mut sorted = columns
1576 .iter()
1577 .map(|c| Sorted {
1578 column: PlSmallStr::from_str(c.as_str()),
1579 descending: Some(false),
1580 nulls_last: Some(false),
1581 })
1582 .collect::<Vec<_>>();
1583
1584 if !columns.is_empty() {
1585 if descending.len() != 1 {
1586 sorted
1587 .iter_mut()
1588 .zip(descending)
1589 .for_each(|(s, d)| s.descending = Some(d));
1590 } else if descending[0] {
1591 sorted.iter_mut().for_each(|s| s.descending = Some(true));
1592 }
1593
1594 if nulls_last.len() != 1 {
1595 sorted
1596 .iter_mut()
1597 .zip(nulls_last)
1598 .for_each(|(s, d)| s.nulls_last = Some(d));
1599 } else if nulls_last[0] {
1600 sorted.iter_mut().for_each(|s| s.nulls_last = Some(true));
1601 }
1602 }
1603
1604 let out = self
1605 .ldf
1606 .read()
1607 .clone()
1608 .hint(HintIR::Sorted(sorted.into()))
1609 .map_err(PyPolarsErr::from)?;
1610 Ok(out.into())
1611 }
1612}
1613
1614#[pyclass(frozen)]
1615struct PyCollectBatches {
1616 inner: Arc<Mutex<CollectBatches>>,
1617 ldf: LazyFrame,
1618}
1619
1620#[pymethods]
1621impl PyCollectBatches {
1622 fn start(&self) {
1623 self.inner.lock().start();
1624 }
1625
1626 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
1627 slf
1628 }
1629
1630 fn __next__(slf: PyRef<'_, Self>, py: Python) -> PyResult<Option<PyDataFrame>> {
1631 let inner = Arc::clone(&slf.inner);
1632 py.enter_polars(|| PolarsResult::Ok(inner.lock().next().transpose()?.map(PyDataFrame::new)))
1633 }
1634
1635 #[allow(unused_variables)]
1636 #[pyo3(signature = (requested_schema=None))]
1637 fn __arrow_c_stream__<'py>(
1638 &self,
1639 py: Python<'py>,
1640 requested_schema: Option<Py<PyAny>>,
1641 ) -> PyResult<Bound<'py, PyCapsule>> {
1642 let mut ldf = self.ldf.clone();
1643 let schema = py
1647 .enter_polars(move || ldf.collect_schema())?
1648 .to_arrow(CompatLevel::newest());
1649
1650 let dtype = ArrowDataType::Struct(schema.into_iter_values().collect());
1651
1652 let iter = Box::new(ArrowStreamIterator::new(self.inner.clone(), dtype.clone()));
1653 let field = ArrowField::new(PlSmallStr::EMPTY, dtype, false);
1654 let stream = export_iterator(iter, field);
1655 PyCapsule::new_with_value(py, stream, c"arrow_array_stream")
1656 }
1657}
1658
1659pub struct ArrowStreamIterator {
1660 inner: Arc<Mutex<CollectBatches>>,
1661 dtype: ArrowDataType,
1662}
1663
1664impl ArrowStreamIterator {
1665 fn new(inner: Arc<Mutex<CollectBatches>>, schema: ArrowDataType) -> Self {
1666 Self {
1667 inner,
1668 dtype: schema,
1669 }
1670 }
1671}
1672
1673impl Iterator for ArrowStreamIterator {
1674 type Item = PolarsResult<ArrayRef>;
1675
1676 fn next(&mut self) -> Option<Self::Item> {
1677 let next = self.inner.lock().next();
1678 match next {
1679 None => None,
1680 Some(Err(err)) => Some(Err(err)),
1681 Some(Ok(df)) => {
1682 let height = df.height();
1683 let arrays = df.rechunk_into_arrow(CompatLevel::newest());
1684 Some(Ok(Box::new(arrow::array::StructArray::new(
1685 self.dtype.clone(),
1686 height,
1687 arrays,
1688 None,
1689 ))))
1690 },
1691 }
1692 }
1693}