1use std::fmt;
2
3use arrow::array::Array;
4use arrow::bitmap::{Bitmap, BitmapBuilder};
5use arrow::datatypes::ArrowDataType;
6use polars_core::prelude::*;
7#[cfg(feature = "parquet")]
8use polars_parquet::read::expr::{ParquetColumnExpr, ParquetScalar, SpecializedParquetColumnExpr};
9use polars_utils::format_pl_smallstr;
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13pub trait PhysicalIoExpr: Send + Sync {
14 fn evaluate_io(&self, df: &DataFrame) -> PolarsResult<Series>;
17}
18
19#[derive(Debug, Clone)]
20pub enum SpecializedColumnPredicate {
21 Equal(Scalar),
22 Between(Scalar, Scalar),
24 EqualOneOf(Box<[Scalar]>),
25 StartsWith(Box<[u8]>),
26 EndsWith(Box<[u8]>),
27 RegexMatch(regex::bytes::Regex),
28}
29
30#[derive(Clone)]
31pub struct ColumnPredicateExpr {
32 column_name: PlSmallStr,
33 dtype: DataType,
34 source_arrow_dtype: ArrowDataType,
35 #[cfg(feature = "parquet")]
36 specialized: Option<SpecializedParquetColumnExpr>,
37 expr: Arc<dyn PhysicalIoExpr>,
38}
39
40impl ColumnPredicateExpr {
41 pub fn new(
42 column_name: PlSmallStr,
43 dtype: DataType,
44 source_arrow_dtype: ArrowDataType,
45 expr: Arc<dyn PhysicalIoExpr>,
46 specialized: Option<SpecializedColumnPredicate>,
47 ) -> Self {
48 use SpecializedColumnPredicate as S;
49 #[cfg(feature = "parquet")]
50 use SpecializedParquetColumnExpr as P;
51 #[cfg(feature = "parquet")]
52 let specialized = specialized.and_then(|s| {
53 Some(match s {
54 S::Equal(s) => P::Equal(cast_to_parquet_scalar(s)?),
55 S::Between(low, high) => {
56 P::Between(cast_to_parquet_scalar(low)?, cast_to_parquet_scalar(high)?)
57 },
58 S::EqualOneOf(scalars) => P::EqualOneOf(
59 scalars
60 .into_iter()
61 .map(|s| cast_to_parquet_scalar(s).ok_or(()))
62 .collect::<Result<Box<_>, ()>>()
63 .ok()?,
64 ),
65 S::StartsWith(s) => P::StartsWith(s),
66 S::EndsWith(s) => P::EndsWith(s),
67 S::RegexMatch(s) => P::RegexMatch(s),
68 })
69 });
70
71 Self {
72 column_name,
73 dtype,
74 source_arrow_dtype,
75 #[cfg(feature = "parquet")]
76 specialized,
77 expr,
78 }
79 }
80}
81
82#[cfg(feature = "parquet")]
83impl ParquetColumnExpr for ColumnPredicateExpr {
84 fn evaluate_mut(&self, values: &dyn Array, bm: &mut BitmapBuilder) {
85 assert!(values.validity().is_none_or(|v| v.set_bits() == 0));
87
88 let series = predicate_values_to_series(
90 self.column_name.clone(),
91 values,
92 &self.dtype,
93 &self.source_arrow_dtype,
94 )
95 .unwrap();
96 let column = series.into_column();
97 let df = unsafe { DataFrame::new_unchecked(values.len(), vec![column]) };
98
99 let true_mask = self.expr.evaluate_io(&df).unwrap();
101 let true_mask = true_mask.bool().unwrap();
102
103 bm.reserve(true_mask.len());
104 for chunk in true_mask.downcast_iter() {
105 match chunk.validity() {
106 None => bm.extend_from_bitmap(chunk.values()),
107 Some(v) => bm.extend_from_bitmap(&(chunk.values() & v)),
108 }
109 }
110 }
111 fn evaluate_null(&self) -> bool {
112 let column = Column::full_null(self.column_name.clone(), 1, &self.dtype);
113 let df = unsafe { DataFrame::new_unchecked(1, vec![column]) };
114
115 let true_mask = self.expr.evaluate_io(&df).unwrap();
117 let true_mask = true_mask.bool().unwrap();
118
119 true_mask.get(0).unwrap_or(false)
120 }
121
122 fn as_specialized(&self) -> Option<&SpecializedParquetColumnExpr> {
123 self.specialized.as_ref()
124 }
125}
126
127#[cfg(feature = "parquet")]
128fn predicate_values_to_series(
129 name: PlSmallStr,
130 values: &dyn Array,
131 dtype: &DataType,
132 source_arrow_dtype: &ArrowDataType,
133) -> PolarsResult<Series> {
134 let timestamp_units_differ = matches!(
137 (source_arrow_dtype, dtype),
138 (
139 ArrowDataType::Timestamp(source_unit, _),
140 DataType::Datetime(target_unit, _),
141 ) if source_unit != &target_unit.to_arrow()
142 );
143
144 if timestamp_units_differ {
145 let values = polars_compute::cast::cast(
146 values,
147 source_arrow_dtype,
148 polars_compute::cast::CastOptionsImpl::default(),
149 )?;
150 Series::try_from((name, values))
151 } else {
152 Series::from_chunk_and_dtype(name, values.to_boxed(), dtype)
153 }
154}
155
156#[cfg(feature = "parquet")]
157fn cast_to_parquet_scalar(scalar: Scalar) -> Option<ParquetScalar> {
158 use AnyValue as A;
159 use ParquetScalar as P;
160
161 Some(match scalar.into_value() {
162 A::Null => P::Null,
163 A::Boolean(v) => P::Boolean(v),
164
165 A::UInt8(v) => P::UInt8(v),
166 A::UInt16(v) => P::UInt16(v),
167 A::UInt32(v) => P::UInt32(v),
168 A::UInt64(v) => P::UInt64(v),
169
170 A::Int8(v) => P::Int8(v),
171 A::Int16(v) => P::Int16(v),
172 A::Int32(v) => P::Int32(v),
173 A::Int64(v) => P::Int64(v),
174
175 #[cfg(feature = "dtype-time")]
176 A::Date(v) => P::Int32(v),
177 #[cfg(feature = "dtype-datetime")]
178 A::Datetime(v, _, _) | A::DatetimeOwned(v, _, _) => P::Int64(v),
179 #[cfg(feature = "dtype-duration")]
180 A::Duration(v, _) => P::Int64(v),
181 #[cfg(feature = "dtype-time")]
182 A::Time(v) => P::Int64(v),
183
184 A::Float32(v) => P::Float32(v),
185 A::Float64(v) => P::Float64(v),
186
187 #[cfg(feature = "dtype-categorical")]
189 A::Categorical(_, _) | A::CategoricalOwned(_, _) | A::Enum(_, _) | A::EnumOwned(_, _) => {
190 return None;
191 },
192
193 A::String(v) => P::String(v.into()),
194 A::StringOwned(v) => P::String(v.as_str().into()),
195 A::Binary(v) => P::Binary(v.into()),
196 A::BinaryOwned(v) => P::Binary(v.into()),
197 _ => return None,
198 })
199}
200
201#[cfg(any(feature = "parquet", feature = "ipc"))]
202pub fn apply_predicate(
203 df: &mut DataFrame,
204 predicate: Option<&dyn PhysicalIoExpr>,
205 parallel: bool,
206) -> PolarsResult<()> {
207 if let (Some(predicate), false) = (&predicate, df.columns().is_empty()) {
208 let s = predicate.evaluate_io(df)?;
209 let mask = s.bool().expect("filter predicates was not of type boolean");
210
211 if parallel {
212 *df = df.filter(mask)?;
213 } else {
214 *df = df.filter_seq(mask)?;
215 }
216 }
217 Ok(())
218}
219
220#[derive(Debug, Clone)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct ColumnStats {
229 field: Field,
230 null_count: Option<Series>,
232 min_value: Option<Series>,
233 max_value: Option<Series>,
234}
235
236impl ColumnStats {
237 pub fn new(
239 field: Field,
240 null_count: Option<Series>,
241 min_value: Option<Series>,
242 max_value: Option<Series>,
243 ) -> Self {
244 Self {
245 field,
246 null_count,
247 min_value,
248 max_value,
249 }
250 }
251
252 pub fn from_field(field: Field) -> Self {
254 Self {
255 field,
256 null_count: None,
257 min_value: None,
258 max_value: None,
259 }
260 }
261
262 pub fn from_column_literal(s: Series) -> Self {
264 debug_assert_eq!(s.len(), 1);
265 Self {
266 field: s.field().into_owned(),
267 null_count: None,
268 min_value: Some(s.clone()),
269 max_value: Some(s),
270 }
271 }
272
273 pub fn field_name(&self) -> &PlSmallStr {
274 self.field.name()
275 }
276
277 pub fn dtype(&self) -> &DataType {
279 self.field.dtype()
280 }
281
282 pub fn get_null_count_state(&self) -> Option<&Series> {
284 self.null_count.as_ref()
285 }
286
287 pub fn get_min_state(&self) -> Option<&Series> {
289 self.min_value.as_ref()
290 }
291
292 pub fn get_max_state(&self) -> Option<&Series> {
294 self.max_value.as_ref()
295 }
296
297 pub fn null_count(&self) -> Option<usize> {
299 match self.dtype() {
300 #[cfg(feature = "dtype-struct")]
301 DataType::Struct(_) => None,
302 _ => {
303 let s = self.get_null_count_state()?;
304 if s.null_count() != s.len() {
306 s.sum().ok()
307 } else {
308 None
309 }
310 },
311 }
312 }
313
314 pub fn to_min_max(&self) -> Option<Series> {
316 let min_val = self.get_min_state()?;
317 let max_val = self.get_max_state()?;
318 let dtype = self.dtype();
319
320 if !use_min_max(dtype) {
321 return None;
322 }
323
324 let mut min_max_values = min_val.clone();
325 min_max_values.append(max_val).unwrap();
326 if min_max_values.null_count() > 0 {
327 None
328 } else {
329 Some(min_max_values)
330 }
331 }
332
333 pub fn to_min(&self) -> Option<&Series> {
337 let min_val = self.min_value.as_ref()?;
339 let dtype = min_val.dtype();
340
341 if !use_min_max(dtype) || min_val.len() != 1 {
342 return None;
343 }
344
345 if min_val.null_count() > 0 {
346 None
347 } else {
348 Some(min_val)
349 }
350 }
351
352 pub fn to_max(&self) -> Option<&Series> {
356 let max_val = self.max_value.as_ref()?;
358 let dtype = max_val.dtype();
359
360 if !use_min_max(dtype) || max_val.len() != 1 {
361 return None;
362 }
363
364 if max_val.null_count() > 0 {
365 None
366 } else {
367 Some(max_val)
368 }
369 }
370}
371
372fn use_min_max(dtype: &DataType) -> bool {
374 dtype.is_primitive_numeric()
375 || dtype.is_temporal()
376 || matches!(
377 dtype,
378 DataType::String | DataType::Binary | DataType::Boolean
379 )
380}
381
382pub struct ColumnStatistics {
383 pub dtype: DataType,
384 pub min: AnyValue<'static>,
385 pub max: AnyValue<'static>,
386 pub null_count: Option<IdxSize>,
387}
388
389pub trait SkipBatchPredicate: Send + Sync {
390 fn schema(&self) -> &SchemaRef;
391
392 fn can_skip_batch(
393 &self,
394 batch_size: IdxSize,
395 live_columns: &PlIndexSet<PlSmallStr>,
396 mut statistics: PlIndexMap<PlSmallStr, ColumnStatistics>,
397 ) -> PolarsResult<bool> {
398 let mut columns = Vec::with_capacity(1 + live_columns.len() * 3);
399
400 columns.push(Column::new_scalar(
401 PlSmallStr::from_static("len"),
402 Scalar::new(IDX_DTYPE, batch_size.into()),
403 1,
404 ));
405
406 for col in live_columns.iter() {
407 let dtype = self.schema().get(col).unwrap();
408 let (min, max, nc) = match statistics.swap_remove(col) {
409 None => (
410 Scalar::null(dtype.clone()),
411 Scalar::null(dtype.clone()),
412 Scalar::null(IDX_DTYPE),
413 ),
414 Some(stat) => (
415 Scalar::new(dtype.clone(), stat.min),
416 Scalar::new(dtype.clone(), stat.max),
417 Scalar::new(
418 IDX_DTYPE,
419 stat.null_count.map_or(AnyValue::Null, |nc| nc.into()),
420 ),
421 ),
422 };
423 columns.extend([
424 Column::new_scalar(format_pl_smallstr!("{col}_min"), min, 1),
425 Column::new_scalar(format_pl_smallstr!("{col}_max"), max, 1),
426 Column::new_scalar(format_pl_smallstr!("{col}_nc"), nc, 1),
427 ]);
428 }
429
430 let df = unsafe { DataFrame::new_unchecked(1, columns) };
434 Ok(self.evaluate_with_stat_df(&df)?.get_bit(0))
435 }
436 fn evaluate_with_stat_df(&self, df: &DataFrame) -> PolarsResult<Bitmap>;
437}
438
439#[derive(Clone)]
440pub struct ColumnPredicates {
441 pub predicates:
442 PlHashMap<PlSmallStr, (Arc<dyn PhysicalIoExpr>, Option<SpecializedColumnPredicate>)>,
443 pub is_sumwise_complete: bool,
444}
445
446#[allow(clippy::derivable_impls)]
448impl Default for ColumnPredicates {
449 fn default() -> Self {
450 Self {
451 predicates: PlHashMap::default(),
452 is_sumwise_complete: false,
453 }
454 }
455}
456
457pub struct PhysicalExprWithConstCols<T> {
458 constants: Vec<(PlSmallStr, Scalar)>,
459 child: T,
460}
461
462impl SkipBatchPredicate for PhysicalExprWithConstCols<Arc<dyn SkipBatchPredicate>> {
463 fn schema(&self) -> &SchemaRef {
464 self.child.schema()
465 }
466
467 fn evaluate_with_stat_df(&self, df: &DataFrame) -> PolarsResult<Bitmap> {
468 let mut df = df.clone();
469 for (name, scalar) in self.constants.iter() {
470 df.with_column(Column::new_scalar(
471 name.clone(),
472 scalar.clone(),
473 df.height(),
474 ))?;
475 }
476 self.child.evaluate_with_stat_df(&df)
477 }
478}
479
480impl PhysicalIoExpr for PhysicalExprWithConstCols<Arc<dyn PhysicalIoExpr>> {
481 fn evaluate_io(&self, df: &DataFrame) -> PolarsResult<Series> {
482 let mut df = df.clone();
483 for (name, scalar) in self.constants.iter() {
484 df.with_column(Column::new_scalar(
485 name.clone(),
486 scalar.clone(),
487 df.height(),
488 ))?;
489 }
490
491 self.child.evaluate_io(&df)
492 }
493}
494
495#[derive(Clone)]
496pub struct ScanIOPredicate {
497 pub predicate: Arc<dyn PhysicalIoExpr>,
498
499 pub live_columns: Arc<PlIndexSet<PlSmallStr>>,
501
502 pub skip_batch_predicate: Option<Arc<dyn SkipBatchPredicate>>,
504
505 pub column_predicates: Arc<ColumnPredicates>,
507
508 pub hive_predicate: Option<Arc<dyn PhysicalIoExpr>>,
510
511 pub hive_predicate_is_full_predicate: bool,
512}
513
514impl ScanIOPredicate {
515 pub fn set_external_constant_columns(&mut self, constant_columns: Vec<(PlSmallStr, Scalar)>) {
516 if constant_columns.is_empty() {
517 return;
518 }
519
520 let mut live_columns = self.live_columns.as_ref().clone();
521 for (c, _) in constant_columns.iter() {
522 live_columns.swap_remove(c);
523 }
524 self.live_columns = Arc::new(live_columns);
525
526 if let Some(skip_batch_predicate) = self.skip_batch_predicate.take() {
527 let mut sbp_constant_columns = Vec::with_capacity(constant_columns.len() * 3);
528 for (c, v) in constant_columns.iter() {
529 sbp_constant_columns.push((format_pl_smallstr!("{c}_min"), v.clone()));
530 sbp_constant_columns.push((format_pl_smallstr!("{c}_max"), v.clone()));
531 let nc = if v.is_null() {
532 AnyValue::Null
533 } else {
534 (0 as IdxSize).into()
535 };
536 sbp_constant_columns
537 .push((format_pl_smallstr!("{c}_nc"), Scalar::new(IDX_DTYPE, nc)));
538 }
539 self.skip_batch_predicate = Some(Arc::new(PhysicalExprWithConstCols {
540 constants: sbp_constant_columns,
541 child: skip_batch_predicate,
542 }));
543 }
544
545 let mut column_predicates = self.column_predicates.as_ref().clone();
546 for (c, _) in constant_columns.iter() {
547 column_predicates.predicates.remove(c);
548 }
549 self.column_predicates = Arc::new(column_predicates);
550
551 self.predicate = Arc::new(PhysicalExprWithConstCols {
552 constants: constant_columns,
553 child: self.predicate.clone(),
554 });
555 }
556}
557
558impl fmt::Debug for ScanIOPredicate {
559 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560 f.write_str("scan_io_predicate")
561 }
562}