Skip to main content

table/
predicate.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17use arc_swap::ArcSwap;
18use common_telemetry::{debug, warn};
19use common_time::Timestamp;
20use common_time::range::TimestampRange;
21use common_time::timestamp::TimeUnit;
22use datafusion::common::ScalarValue;
23use datafusion::physical_optimizer::pruning::PruningPredicate;
24use datafusion_common::ToDFSchema;
25use datafusion_common::pruning::PruningStatistics;
26use datafusion_common::tree_node::TreeNode;
27use datafusion_expr::expr::{Expr, InList};
28use datafusion_expr::{Between, BinaryExpr, Operator};
29use datafusion_physical_expr::execution_props::ExecutionProps;
30use datafusion_physical_expr::expressions::DynamicFilterPhysicalExpr;
31use datafusion_physical_expr::{PhysicalExpr, create_physical_expr};
32use datatypes::arrow;
33use datatypes::value::scalar_value_to_timestamp;
34use snafu::ResultExt;
35
36use crate::error;
37
38#[cfg(test)]
39mod stats;
40
41/// Assert the scalar value is not utf8. Returns `None` if it's utf8.
42/// In theory, it should be converted to a timestamp scalar value by `TypeConversionRule`.
43macro_rules! return_none_if_utf8 {
44    ($lit: ident) => {
45        if is_string_timestamp_literal($lit) {
46            warn!(
47                "Unexpected ScalarValue::Utf8 in time range predicate: {:?}. Maybe it's an implicit bug, please report it to https://github.com/GreptimeTeam/greptimedb/issues",
48                $lit
49            );
50
51            // Make the predicate ineffective.
52            return None;
53        }
54    };
55}
56
57pub fn is_string_timestamp_literal(scalar: &ScalarValue) -> bool {
58    matches!(
59        scalar,
60        ScalarValue::Utf8(_) | ScalarValue::LargeUtf8(_) | ScalarValue::Utf8View(_)
61    )
62}
63
64/// Reference-counted pointer to a list of logical exprs and a list of dynamic filter physical exprs.
65#[derive(Debug, Clone, Default)]
66pub struct Predicate {
67    /// logical exprs
68    exprs: Arc<Vec<Expr>>,
69    /// dynamic filter physical exprs, only useful if dynamic filtering is enabled
70    ///
71    /// They are usually from `TopK` or `Join` operators, and can dynamically filter data during query execution by using current runtime information to further reduce data scanning
72    dyn_filters: Arc<ArcSwap<Vec<Arc<DynamicFilterPhysicalExpr>>>>,
73}
74
75impl Predicate {
76    /// Creates a new `Predicate` by converting logical exprs to physical exprs that can be
77    /// evaluated against record batches.
78    /// Returns error when failed to convert exprs.
79    pub fn new(exprs: Vec<Expr>) -> Self {
80        Self {
81            exprs: Arc::new(exprs),
82            dyn_filters: Arc::new(ArcSwap::new(Arc::new(vec![]))),
83        }
84    }
85
86    pub fn with_dyn_filters(
87        exprs: Vec<Expr>,
88        dyn_filters: Vec<Arc<DynamicFilterPhysicalExpr>>,
89    ) -> Self {
90        Self {
91            exprs: Arc::new(exprs),
92            dyn_filters: Arc::new(ArcSwap::new(Arc::new(dyn_filters))),
93        }
94    }
95
96    pub fn is_empty(&self) -> bool {
97        self.exprs.is_empty() && self.dyn_filters.load().is_empty()
98    }
99
100    /// Adds dynamic filter physical exprs to the existing list.
101    pub fn add_dyn_filters(&self, dyn_filters: Vec<Arc<DynamicFilterPhysicalExpr>>) {
102        self.dyn_filters.rcu(|existing| {
103            let mut new_filters = existing.as_ref().clone();
104            new_filters.extend(dyn_filters.clone());
105            Arc::new(new_filters)
106        });
107    }
108
109    /// Returns the logical exprs.
110    pub fn exprs(&self) -> &[Expr] {
111        &self.exprs
112    }
113
114    /// Returns the dynamic filter physical exprs. Notice this return a live dynamic filters which
115    /// can change during query execution.
116    pub fn dyn_filters(&self) -> Arc<Vec<Arc<DynamicFilterPhysicalExpr>>> {
117        self.dyn_filters.load_full()
118    }
119
120    /// Returns the dynamic filter as physical exprs. Notice this return a "snapshot" of
121    /// dynamic filters at the time of calling this method.
122    pub fn dyn_filter_phy_exprs(&self) -> error::Result<Vec<Arc<dyn PhysicalExpr>>> {
123        self.dyn_filters
124            .load()
125            .iter()
126            .map(|e| e.current())
127            .collect::<Result<Vec<_>, _>>()
128            .context(error::DatafusionSnafu)
129    }
130
131    /// Builds a single physical expr according to provided schema.
132    pub fn to_physical_expr(
133        expr: &Expr,
134        schema: &arrow::datatypes::SchemaRef,
135    ) -> error::Result<Arc<dyn PhysicalExpr>> {
136        let df_schema = schema
137            .clone()
138            .to_dfschema_ref()
139            .context(error::DatafusionSnafu)?;
140
141        // TODO(hl): `execution_props` provides variables required by evaluation.
142        // we may reuse the `execution_props` from `SessionState` once we support
143        // registering variables.
144        let execution_props = &ExecutionProps::new();
145
146        create_physical_expr(expr, df_schema.as_ref(), execution_props)
147            .context(error::DatafusionSnafu)
148    }
149
150    /// Builds physical exprs according to provided schema.
151    pub fn to_physical_exprs(
152        &self,
153        schema: &arrow::datatypes::SchemaRef,
154    ) -> error::Result<Vec<Arc<dyn PhysicalExpr>>> {
155        let dyn_filters = self.dyn_filter_phy_exprs()?;
156
157        Ok(self
158            .exprs
159            .iter()
160            .filter_map(|expr| Self::to_physical_expr(expr, schema).ok())
161            .chain(dyn_filters)
162            .collect::<Vec<_>>())
163    }
164
165    /// Evaluates the predicate against the `stats`.
166    /// Returns a vector of boolean values, among which `false` means the row group can be skipped.
167    pub fn prune_with_stats<S: PruningStatistics>(
168        &self,
169        stats: &S,
170        schema: &arrow::datatypes::SchemaRef,
171    ) -> Vec<bool> {
172        let mut res = vec![true; stats.num_containers()];
173        let physical_exprs = match self.to_physical_exprs(schema) {
174            Ok(expr) => expr,
175            Err(e) => {
176                warn!(e; "Failed to build physical expr from predicates: {:?}", &self.exprs);
177                return res;
178            }
179        };
180
181        for expr in &physical_exprs {
182            match PruningPredicate::try_new(expr.clone(), schema.clone()) {
183                Ok(p) => match p.prune(stats) {
184                    Ok(r) => {
185                        for (curr_val, res) in r.into_iter().zip(res.iter_mut()) {
186                            *res &= curr_val
187                        }
188                    }
189                    Err(e) => {
190                        warn!(e; "Failed to prune row groups");
191                    }
192                },
193                Err(e) => {
194                    // since dynamic filter exprs could be complex, it's possible that `PruningPredicate::try_new` fails to prove anything from it. In that case, we just log it and skip pruning with this expr.
195                    debug!("Failed to create pruning predicate for expr: {e:?}");
196                }
197            }
198        }
199        res
200    }
201}
202
203// tests for `build_time_range_predicate` locates in src/query/tests/time_range_filter_test.rs
204// since it requires query engine to convert sql to filters.
205/// `build_time_range_predicate` extracts time range from logical exprs to facilitate fast
206/// time range pruning.
207pub fn build_time_range_predicate(
208    ts_col_name: &str,
209    ts_col_unit: TimeUnit,
210    filters: &[Expr],
211) -> TimestampRange {
212    let mut res = TimestampRange::min_to_max();
213    for expr in filters {
214        if let Some(range) = extract_time_range_from_expr(ts_col_name, ts_col_unit, expr) {
215            res = res.and(&range);
216        }
217    }
218    res
219}
220
221/// The outcome of strictly extracting the time range of `ts_col_name` from a
222/// scan's filters. Unlike [`build_time_range_predicate`], which quietly widens
223/// to `min_to_max` on anything it cannot parse (fine for pruning), this
224/// distinguishes "the column is not filtered at all" from "it is filtered in a
225/// way that cannot be safely turned into a range" — for callers whose contract
226/// forbids silently falling back to a default window.
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub enum TimeRangeExtraction {
229    /// No filter references the column.
230    Absent,
231    /// Every filter referencing the column was folded into this range, which
232    /// over-approximates the filters' satisfying set.
233    Extracted(TimestampRange),
234    /// At least one filter references the column in a shape that cannot be
235    /// safely extracted (e.g. under `OR`/`NOT`, or compared to a non-literal).
236    Unsupported,
237}
238
239/// Strictly extracts the time range of `ts_col_name` from the (implicitly
240/// AND-ed) scan filters. See [`TimeRangeExtraction`].
241pub fn extract_time_range_strict(
242    ts_col_name: &str,
243    ts_col_unit: TimeUnit,
244    filters: &[Expr],
245) -> TimeRangeExtraction {
246    let mut range: Option<TimestampRange> = None;
247    for expr in filters {
248        if !expr
249            .column_refs()
250            .iter()
251            .any(|column| column.name == ts_col_name)
252        {
253            continue;
254        }
255        // Disjunctive shapes (`OR`, `IN`) collapse disjoint ranges into their
256        // convex hull, which is not exactly representable as one contiguous
257        // range; callers deriving synthetic timestamps from the bounds would
258        // emit points inside the gaps.
259        if contains_disjunction_over_column(expr, ts_col_name) {
260            return TimeRangeExtraction::Unsupported;
261        }
262        // `Some` from the lenient extractor over-approximates the expression's
263        // satisfying set (an `AND` side it cannot parse is dropped, which only
264        // widens), so intersecting extracted conjuncts stays an
265        // over-approximation.
266        match extract_time_range_from_expr(ts_col_name, ts_col_unit, expr) {
267            Some(extracted) => {
268                range = Some(match range {
269                    Some(acc) => acc.and(&extracted),
270                    None => extracted,
271                });
272            }
273            None => return TimeRangeExtraction::Unsupported,
274        }
275    }
276    match range {
277        Some(range) => TimeRangeExtraction::Extracted(range),
278        None => TimeRangeExtraction::Absent,
279    }
280}
281
282fn contains_disjunction_over_column(expr: &Expr, ts_col_name: &str) -> bool {
283    let is_disjunction = |expr: &Expr| {
284        matches!(
285            expr,
286            Expr::BinaryExpr(BinaryExpr {
287                op: Operator::Or,
288                ..
289            }) | Expr::InList(_)
290        ) && expr
291            .column_refs()
292            .iter()
293            .any(|column| column.name == ts_col_name)
294    };
295    expr.exists(|expr| Ok(is_disjunction(expr))).unwrap_or(true)
296}
297
298/// Extract time range filter from `WHERE`/`IN (...)`/`BETWEEN` clauses.
299/// Return None if no time range can be found in expr.
300pub fn extract_time_range_from_expr(
301    ts_col_name: &str,
302    ts_col_unit: TimeUnit,
303    expr: &Expr,
304) -> Option<TimestampRange> {
305    match expr {
306        Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
307            extract_from_binary_expr(ts_col_name, ts_col_unit, left, op, right)
308        }
309        Expr::Between(Between {
310            expr,
311            negated,
312            low,
313            high,
314        }) => extract_from_between_expr(ts_col_name, ts_col_unit, expr, negated, low, high),
315        Expr::InList(InList {
316            expr,
317            list,
318            negated,
319        }) => extract_from_in_list_expr(ts_col_name, expr, *negated, list),
320        _ => None,
321    }
322}
323
324fn extract_from_binary_expr(
325    ts_col_name: &str,
326    ts_col_unit: TimeUnit,
327    left: &Expr,
328    op: &Operator,
329    right: &Expr,
330) -> Option<TimestampRange> {
331    match op {
332        Operator::Eq => get_timestamp_filter(ts_col_name, left, right)
333            .and_then(|(ts, _)| ts.convert_to(ts_col_unit))
334            .map(TimestampRange::single),
335        Operator::Lt => {
336            let (ts, reverse) = get_timestamp_filter(ts_col_name, left, right)?;
337            if reverse {
338                // [lit] < ts_col
339                let ts_val = ts.convert_to(ts_col_unit)?.value();
340                Some(TimestampRange::from_start(Timestamp::new(
341                    ts_val + 1,
342                    ts_col_unit,
343                )))
344            } else {
345                // ts_col < [lit]
346                ts.convert_to_ceil(ts_col_unit)
347                    .map(|ts| TimestampRange::until_end(ts, false))
348            }
349        }
350        Operator::LtEq => {
351            let (ts, reverse) = get_timestamp_filter(ts_col_name, left, right)?;
352            if reverse {
353                // [lit] <= ts_col
354                ts.convert_to_ceil(ts_col_unit)
355                    .map(TimestampRange::from_start)
356            } else {
357                // ts_col <= [lit]
358                ts.convert_to(ts_col_unit)
359                    .map(|ts| TimestampRange::until_end(ts, true))
360            }
361        }
362        Operator::Gt => {
363            let (ts, reverse) = get_timestamp_filter(ts_col_name, left, right)?;
364            if reverse {
365                // [lit] > ts_col
366                ts.convert_to_ceil(ts_col_unit)
367                    .map(|t| TimestampRange::until_end(t, false))
368            } else {
369                // ts_col > [lit]
370                let ts_val = ts.convert_to(ts_col_unit)?.value();
371                Some(TimestampRange::from_start(Timestamp::new(
372                    ts_val + 1,
373                    ts_col_unit,
374                )))
375            }
376        }
377        Operator::GtEq => {
378            let (ts, reverse) = get_timestamp_filter(ts_col_name, left, right)?;
379            if reverse {
380                // [lit] >= ts_col
381                ts.convert_to(ts_col_unit)
382                    .map(|t| TimestampRange::until_end(t, true))
383            } else {
384                // ts_col >= [lit]
385                ts.convert_to_ceil(ts_col_unit)
386                    .map(TimestampRange::from_start)
387            }
388        }
389        Operator::And => {
390            // instead of return none when failed to extract time range from left/right, we unwrap the none into
391            // `TimestampRange::min_to_max`.
392            let left = extract_time_range_from_expr(ts_col_name, ts_col_unit, left)
393                .unwrap_or_else(TimestampRange::min_to_max);
394            let right = extract_time_range_from_expr(ts_col_name, ts_col_unit, right)
395                .unwrap_or_else(TimestampRange::min_to_max);
396            Some(left.and(&right))
397        }
398        Operator::Or => {
399            let left = extract_time_range_from_expr(ts_col_name, ts_col_unit, left)?;
400            let right = extract_time_range_from_expr(ts_col_name, ts_col_unit, right)?;
401            Some(left.or(&right))
402        }
403        _ => None,
404    }
405}
406
407fn get_timestamp_filter(ts_col_name: &str, left: &Expr, right: &Expr) -> Option<(Timestamp, bool)> {
408    let (col, lit, reverse) = match (left, right) {
409        (Expr::Column(column), Expr::Literal(scalar, _)) => (column, scalar, false),
410        (Expr::Literal(scalar, _), Expr::Column(column)) => (column, scalar, true),
411        _ => {
412            return None;
413        }
414    };
415    if col.name != ts_col_name {
416        return None;
417    }
418
419    return_none_if_utf8!(lit);
420    scalar_value_to_timestamp(lit, None).map(|t| (t, reverse))
421}
422
423fn extract_from_between_expr(
424    ts_col_name: &str,
425    ts_col_unit: TimeUnit,
426    expr: &Expr,
427    negated: &bool,
428    low: &Expr,
429    high: &Expr,
430) -> Option<TimestampRange> {
431    let Expr::Column(col) = expr else {
432        return None;
433    };
434    if col.name != ts_col_name {
435        return None;
436    }
437
438    if *negated {
439        return None;
440    }
441
442    match (low, high) {
443        (Expr::Literal(low, _), Expr::Literal(high, _)) => {
444            return_none_if_utf8!(low);
445            return_none_if_utf8!(high);
446
447            let low_opt =
448                scalar_value_to_timestamp(low, None).and_then(|ts| ts.convert_to(ts_col_unit));
449            let high_opt = scalar_value_to_timestamp(high, None)
450                .and_then(|ts| ts.convert_to_ceil(ts_col_unit));
451            Some(TimestampRange::new_inclusive(low_opt, high_opt))
452        }
453        _ => None,
454    }
455}
456
457/// Extract time range filter from `IN (...)` expr.
458fn extract_from_in_list_expr(
459    ts_col_name: &str,
460    expr: &Expr,
461    negated: bool,
462    list: &[Expr],
463) -> Option<TimestampRange> {
464    if negated {
465        return None;
466    }
467    let Expr::Column(col) = expr else {
468        return None;
469    };
470    if col.name != ts_col_name {
471        return None;
472    }
473
474    if list.is_empty() {
475        return Some(TimestampRange::empty());
476    }
477    let mut init_range = TimestampRange::empty();
478    for expr in list {
479        if let Expr::Literal(scalar, _) = expr {
480            return_none_if_utf8!(scalar);
481            if let Some(timestamp) = scalar_value_to_timestamp(scalar, None) {
482                init_range = init_range.or(&TimestampRange::single(timestamp))
483            } else {
484                // TODO(hl): maybe we should raise an error here since cannot parse
485                // timestamp value from in list expr
486                return None;
487            }
488        }
489    }
490    Some(init_range)
491}
492
493#[cfg(test)]
494mod tests {
495    use std::sync::Arc;
496
497    use common_test_util::temp_dir::{TempDir, create_temp_dir};
498    use datafusion::parquet::arrow::ArrowWriter;
499    use datafusion_common::{Column, ScalarValue};
500    use datafusion_expr::{BinaryExpr, Literal, Operator, col, lit};
501    use datatypes::arrow::array::Int32Array;
502    use datatypes::arrow::datatypes::{DataType, Field, Schema};
503    use datatypes::arrow::record_batch::RecordBatch;
504    use datatypes::arrow_array::StringArray;
505    use parquet::arrow::ParquetRecordBatchStreamBuilder;
506    use parquet::file::properties::WriterProperties;
507
508    use super::*;
509    use crate::predicate::stats::RowGroupPruningStatistics;
510
511    fn check_build_predicate(expr: Expr, expect: TimestampRange) {
512        assert_eq!(
513            expect,
514            build_time_range_predicate("ts", TimeUnit::Millisecond, &[expr])
515        );
516    }
517
518    #[test]
519    fn test_gt() {
520        // ts > 1ms
521        check_build_predicate(
522            col("ts").gt(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
523            TimestampRange::from_start(Timestamp::new_millisecond(2)),
524        );
525
526        // 1ms > ts
527        check_build_predicate(
528            lit(ScalarValue::TimestampMillisecond(Some(1), None)).gt(col("ts")),
529            TimestampRange::until_end(Timestamp::new_millisecond(1), false),
530        );
531
532        // 1001us > ts
533        check_build_predicate(
534            lit(ScalarValue::TimestampMicrosecond(Some(1001), None)).gt(col("ts")),
535            TimestampRange::until_end(Timestamp::new_millisecond(1), true),
536        );
537
538        // ts > 1001us
539        check_build_predicate(
540            col("ts").gt(lit(ScalarValue::TimestampMicrosecond(Some(1001), None))),
541            TimestampRange::from_start(Timestamp::new_millisecond(2)),
542        );
543
544        // 1s > ts
545        check_build_predicate(
546            lit(ScalarValue::TimestampSecond(Some(1), None)).gt(col("ts")),
547            TimestampRange::until_end(Timestamp::new_millisecond(1000), false),
548        );
549
550        // ts > 1s
551        check_build_predicate(
552            col("ts").gt(lit(ScalarValue::TimestampSecond(Some(1), None))),
553            TimestampRange::from_start(Timestamp::new_millisecond(1001)),
554        );
555    }
556
557    #[test]
558    fn test_gt_eq() {
559        // ts >= 1ms
560        check_build_predicate(
561            col("ts").gt_eq(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
562            TimestampRange::from_start(Timestamp::new_millisecond(1)),
563        );
564
565        // 1ms >= ts
566        check_build_predicate(
567            lit(ScalarValue::TimestampMillisecond(Some(1), None)).gt_eq(col("ts")),
568            TimestampRange::until_end(Timestamp::new_millisecond(1), true),
569        );
570
571        // 1001us >= ts
572        check_build_predicate(
573            lit(ScalarValue::TimestampMicrosecond(Some(1001), None)).gt_eq(col("ts")),
574            TimestampRange::until_end(Timestamp::new_millisecond(1), true),
575        );
576
577        // ts >= 1001us
578        check_build_predicate(
579            col("ts").gt_eq(lit(ScalarValue::TimestampMicrosecond(Some(1001), None))),
580            TimestampRange::from_start(Timestamp::new_millisecond(2)),
581        );
582
583        // 1s >= ts
584        check_build_predicate(
585            lit(ScalarValue::TimestampSecond(Some(1), None)).gt_eq(col("ts")),
586            TimestampRange::until_end(Timestamp::new_millisecond(1000), true),
587        );
588
589        // ts >= 1s
590        check_build_predicate(
591            col("ts").gt_eq(lit(ScalarValue::TimestampSecond(Some(1), None))),
592            TimestampRange::from_start(Timestamp::new_millisecond(1000)),
593        );
594    }
595
596    #[test]
597    fn test_lt() {
598        // ts < 1ms
599        check_build_predicate(
600            col("ts").lt(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
601            TimestampRange::until_end(Timestamp::new_millisecond(1), false),
602        );
603
604        // 1ms < ts
605        check_build_predicate(
606            lit(ScalarValue::TimestampMillisecond(Some(1), None)).lt(col("ts")),
607            TimestampRange::from_start(Timestamp::new_millisecond(2)),
608        );
609
610        // 1001us < ts
611        check_build_predicate(
612            lit(ScalarValue::TimestampMicrosecond(Some(1001), None)).lt(col("ts")),
613            TimestampRange::from_start(Timestamp::new_millisecond(2)),
614        );
615
616        // ts < 1001us
617        check_build_predicate(
618            col("ts").lt(lit(ScalarValue::TimestampMicrosecond(Some(1001), None))),
619            TimestampRange::until_end(Timestamp::new_millisecond(1), true),
620        );
621
622        // 1s < ts
623        check_build_predicate(
624            lit(ScalarValue::TimestampSecond(Some(1), None)).lt(col("ts")),
625            TimestampRange::from_start(Timestamp::new_millisecond(1001)),
626        );
627
628        // ts < 1s
629        check_build_predicate(
630            col("ts").lt(lit(ScalarValue::TimestampSecond(Some(1), None))),
631            TimestampRange::until_end(Timestamp::new_millisecond(1000), false),
632        );
633    }
634
635    #[test]
636    fn test_lt_eq() {
637        // ts <= 1ms
638        check_build_predicate(
639            col("ts").lt_eq(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
640            TimestampRange::until_end(Timestamp::new_millisecond(1), true),
641        );
642
643        // 1ms <= ts
644        check_build_predicate(
645            lit(ScalarValue::TimestampMillisecond(Some(1), None)).lt_eq(col("ts")),
646            TimestampRange::from_start(Timestamp::new_millisecond(1)),
647        );
648
649        // 1001us <= ts
650        check_build_predicate(
651            lit(ScalarValue::TimestampMicrosecond(Some(1001), None)).lt_eq(col("ts")),
652            TimestampRange::from_start(Timestamp::new_millisecond(2)),
653        );
654
655        // ts <= 1001us
656        check_build_predicate(
657            col("ts").lt_eq(lit(ScalarValue::TimestampMicrosecond(Some(1001), None))),
658            TimestampRange::until_end(Timestamp::new_millisecond(1), true),
659        );
660
661        // 1s <= ts
662        check_build_predicate(
663            lit(ScalarValue::TimestampSecond(Some(1), None)).lt_eq(col("ts")),
664            TimestampRange::from_start(Timestamp::new_millisecond(1000)),
665        );
666
667        // ts <= 1s
668        check_build_predicate(
669            col("ts").lt_eq(lit(ScalarValue::TimestampSecond(Some(1), None))),
670            TimestampRange::until_end(Timestamp::new_millisecond(1000), true),
671        );
672    }
673
674    #[test]
675    fn test_extract_time_range_strict() {
676        fn ts_lit(ms: i64) -> Expr {
677            lit(ScalarValue::TimestampMillisecond(Some(ms), None))
678        }
679        let extract =
680            |filters: &[Expr]| extract_time_range_strict("ts", TimeUnit::Millisecond, filters);
681        let range = |start: i64, end: i64| {
682            TimestampRange::new(
683                Timestamp::new_millisecond(start),
684                Timestamp::new_millisecond(end),
685            )
686            .unwrap()
687        };
688
689        // No filter references the column.
690        assert_eq!(extract(&[]), TimeRangeExtraction::Absent);
691        assert_eq!(
692            extract(&[col("host").eq(lit("a"))]),
693            TimeRangeExtraction::Absent
694        );
695
696        // Both bounds across conjuncts; unrelated filters are ignored.
697        assert_eq!(
698            extract(&[
699                col("ts").gt_eq(ts_lit(1000)),
700                col("ts").lt(ts_lit(2000)),
701                col("host").eq(lit("a")),
702            ]),
703            TimeRangeExtraction::Extracted(range(1000, 2000))
704        );
705
706        // Lower bound only / upper bound only.
707        assert_eq!(
708            extract(&[col("ts").gt_eq(ts_lit(1000))]),
709            TimeRangeExtraction::Extracted(TimestampRange::from_start(Timestamp::new_millisecond(
710                1000
711            )))
712        );
713        assert_eq!(
714            extract(&[col("ts").lt(ts_lit(2000))]),
715            TimeRangeExtraction::Extracted(TimestampRange::until_end(
716                Timestamp::new_millisecond(2000),
717                false
718            ))
719        );
720
721        // BETWEEN is inclusive on both ends.
722        assert_eq!(
723            extract(&[col("ts").between(ts_lit(1000), ts_lit(2000))]),
724            TimeRangeExtraction::Extracted(range(1000, 2001))
725        );
726
727        // Equality pins a single point.
728        assert_eq!(
729            extract(&[col("ts").eq(ts_lit(1500))]),
730            TimeRangeExtraction::Extracted(TimestampRange::single(Timestamp::new_millisecond(
731                1500
732            )))
733        );
734
735        // An unparsable side under AND only widens; the extraction stays safe.
736        assert_eq!(
737            extract(&[col("ts").gt_eq(ts_lit(1000)).and(col("ts").lt(col("t2")))]),
738            TimeRangeExtraction::Extracted(TimestampRange::from_start(Timestamp::new_millisecond(
739                1000
740            )))
741        );
742
743        // Contradictory bounds collapse to the empty range, not an error.
744        let TimeRangeExtraction::Extracted(empty) =
745            extract(&[col("ts").gt_eq(ts_lit(2000)), col("ts").lt(ts_lit(1000))])
746        else {
747            panic!("expected an extraction");
748        };
749        assert!(empty.is_empty());
750
751        // Shapes that could widen the satisfying set beyond what is extractable
752        // must be refused: OR with an unparsable side, NOT, non-literal bounds.
753        assert_eq!(
754            extract(&[col("ts").gt(ts_lit(1000)).or(col("host").eq(lit("a")))]),
755            TimeRangeExtraction::Unsupported
756        );
757        assert_eq!(
758            extract(&[!col("ts").gt(ts_lit(1000))]),
759            TimeRangeExtraction::Unsupported
760        );
761        assert_eq!(
762            extract(&[col("ts").gt_eq(col("t2"))]),
763            TimeRangeExtraction::Unsupported
764        );
765    }
766
767    async fn gen_test_parquet_file(dir: &TempDir, cnt: usize) -> (String, Arc<Schema>) {
768        let path = dir
769            .path()
770            .join("test-prune.parquet")
771            .to_string_lossy()
772            .to_string();
773
774        let name_field = Field::new("name", DataType::Utf8, true);
775        let count_field = Field::new("cnt", DataType::Int32, true);
776        let schema = Arc::new(Schema::new(vec![name_field, count_field]));
777
778        let file = std::fs::OpenOptions::new()
779            .write(true)
780            .create(true)
781            .truncate(true)
782            .open(path.clone())
783            .unwrap();
784
785        let write_props = WriterProperties::builder()
786            .set_max_row_group_row_count(Some(10))
787            .build();
788        let mut writer = ArrowWriter::try_new(file, schema.clone(), Some(write_props)).unwrap();
789
790        for i in (0..cnt).step_by(10) {
791            let name_array = Arc::new(StringArray::from(
792                (i..(i + 10).min(cnt))
793                    .map(|i| i.to_string())
794                    .collect::<Vec<_>>(),
795            )) as Arc<_>;
796            let count_array = Arc::new(Int32Array::from(
797                (i..(i + 10).min(cnt)).map(|i| i as i32).collect::<Vec<_>>(),
798            )) as Arc<_>;
799            let rb = RecordBatch::try_new(schema.clone(), vec![name_array, count_array]).unwrap();
800            writer.write(&rb).unwrap();
801        }
802        let _ = writer.close().unwrap();
803        (path, schema)
804    }
805
806    async fn assert_prune(array_cnt: usize, filters: Vec<Expr>, expect: Vec<bool>) {
807        let dir = create_temp_dir("prune_parquet");
808        let (path, arrow_schema) = gen_test_parquet_file(&dir, array_cnt).await;
809        let schema = Arc::new(datatypes::schema::Schema::try_from(arrow_schema.clone()).unwrap());
810        let arrow_predicate = Predicate::new(filters);
811        let builder = ParquetRecordBatchStreamBuilder::new(
812            tokio::fs::OpenOptions::new()
813                .read(true)
814                .open(path)
815                .await
816                .unwrap(),
817        )
818        .await
819        .unwrap();
820        let metadata = builder.metadata().clone();
821        let row_groups = metadata.row_groups();
822
823        let stats = RowGroupPruningStatistics::new(row_groups, &schema);
824        let res = arrow_predicate.prune_with_stats(&stats, &arrow_schema);
825        assert_eq!(expect, res);
826    }
827
828    fn gen_predicate(max_val: i32, op: Operator) -> Vec<Expr> {
829        vec![datafusion_expr::Expr::BinaryExpr(BinaryExpr {
830            left: Box::new(datafusion_expr::Expr::Column(Column::from_name("cnt"))),
831            op,
832            right: Box::new(max_val.lit()),
833        })]
834    }
835
836    #[tokio::test]
837    async fn test_prune_empty() {
838        assert_prune(3, vec![], vec![true]).await;
839    }
840
841    #[tokio::test]
842    async fn test_prune_all_match() {
843        let p = gen_predicate(3, Operator::Gt);
844        assert_prune(2, p, vec![false]).await;
845    }
846
847    #[tokio::test]
848    async fn test_prune_gt() {
849        let p = gen_predicate(29, Operator::Gt);
850        assert_prune(
851            100,
852            p,
853            vec![
854                false, false, false, true, true, true, true, true, true, true,
855            ],
856        )
857        .await;
858    }
859
860    #[tokio::test]
861    async fn test_prune_eq_expr() {
862        let p = gen_predicate(30, Operator::Eq);
863        assert_prune(40, p, vec![false, false, false, true]).await;
864    }
865
866    #[tokio::test]
867    async fn test_prune_neq_expr() {
868        let p = gen_predicate(30, Operator::NotEq);
869        assert_prune(40, p, vec![true, true, true, true]).await;
870    }
871
872    #[tokio::test]
873    async fn test_prune_gteq_expr() {
874        let p = gen_predicate(29, Operator::GtEq);
875        assert_prune(40, p, vec![false, false, true, true]).await;
876    }
877
878    #[tokio::test]
879    async fn test_prune_lt_expr() {
880        let p = gen_predicate(30, Operator::Lt);
881        assert_prune(40, p, vec![true, true, true, false]).await;
882    }
883
884    #[tokio::test]
885    async fn test_prune_lteq_expr() {
886        let p = gen_predicate(30, Operator::LtEq);
887        assert_prune(40, p, vec![true, true, true, true]).await;
888    }
889
890    #[tokio::test]
891    async fn test_prune_between_expr() {
892        let p = gen_predicate(30, Operator::LtEq);
893        assert_prune(40, p, vec![true, true, true, true]).await;
894    }
895
896    #[tokio::test]
897    async fn test_or() {
898        // cnt > 30 or cnt < 20
899        let e = datafusion_expr::Expr::Column(Column::from_name("cnt"))
900            .gt(30.lit())
901            .or(datafusion_expr::Expr::Column(Column::from_name("cnt")).lt(20.lit()));
902        assert_prune(40, vec![e], vec![true, true, false, true]).await;
903    }
904
905    #[tokio::test]
906    async fn test_to_physical_expr() {
907        let predicate = Predicate::new(vec![
908            col("host").eq(lit("host_a")),
909            col("ts").gt(lit(ScalarValue::TimestampMicrosecond(Some(123), None))),
910        ]);
911
912        let schema = Arc::new(arrow::datatypes::Schema::new(vec![Field::new(
913            "host",
914            arrow::datatypes::DataType::Utf8,
915            false,
916        )]));
917
918        let predicates = predicate.to_physical_exprs(&schema).unwrap();
919        assert!(!predicates.is_empty());
920
921        let physical_expr = Predicate::to_physical_expr(&col("host").eq(lit("host_a")), &schema);
922        assert!(physical_expr.is_ok());
923    }
924}