Skip to main content

datafusion_optimizer/simplify_expressions/
simplify_exprs.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Simplify expressions optimizer rule and implementation
19
20use std::sync::Arc;
21
22use datafusion_common::tree_node::{Transformed, TreeNode};
23use datafusion_common::{Column, DFSchema, DFSchemaRef, DataFusionError, Result};
24use datafusion_expr::logical_plan::{Aggregate, LogicalPlan, Projection};
25use datafusion_expr::simplify::SimplifyContext;
26use datafusion_expr::utils::{
27    columnize_expr, find_aggregate_exprs, grouping_set_to_exprlist, merge_schema,
28};
29use datafusion_expr::{DmlStatement, Expr, WriteOp};
30
31use super::ExprSimplifier;
32use crate::optimizer::ApplyOrder;
33use crate::simplify_expressions::linear_aggregates::rewrite_multiple_linear_aggregates;
34use crate::utils::NamePreserver;
35use crate::{OptimizerConfig, OptimizerRule};
36
37/// Optimizer Pass that simplifies [`LogicalPlan`]s by rewriting
38/// [`Expr`]`s evaluating constants and applying algebraic
39/// simplifications
40///
41/// # Introduction
42/// It uses boolean algebra laws to simplify or reduce the number of terms in expressions.
43///
44/// # Example:
45/// `Filter: b > 2 AND b > 2`
46/// is optimized to
47/// `Filter: b > 2`
48///
49/// [`Expr`]: datafusion_expr::Expr
50#[derive(Default, Debug)]
51pub struct SimplifyExpressions {}
52
53impl OptimizerRule for SimplifyExpressions {
54    fn name(&self) -> &str {
55        "simplify_expressions"
56    }
57
58    fn apply_order(&self) -> Option<ApplyOrder> {
59        Some(ApplyOrder::BottomUp)
60    }
61
62    fn supports_rewrite(&self) -> bool {
63        true
64    }
65
66    fn rewrite(
67        &self,
68        plan: LogicalPlan,
69        config: &dyn OptimizerConfig,
70    ) -> Result<Transformed<LogicalPlan>, DataFusionError> {
71        Self::optimize_internal(plan, config)
72    }
73}
74
75impl SimplifyExpressions {
76    fn optimize_internal(
77        plan: LogicalPlan,
78        config: &dyn OptimizerConfig,
79    ) -> Result<Transformed<LogicalPlan>> {
80        let schema = if let LogicalPlan::Dml(DmlStatement {
81            op: WriteOp::MergeInto(_),
82            table_name,
83            target,
84            ..
85        }) = &plan
86        {
87            let mut schema = merge_schema(&plan.inputs());
88            schema.merge(&DFSchema::try_from_qualified_schema(
89                table_name.clone(),
90                &target.schema(),
91            )?);
92            DFSchemaRef::new(schema)
93        } else if !plan.inputs().is_empty() {
94            DFSchemaRef::new(merge_schema(&plan.inputs()))
95        } else if let LogicalPlan::TableScan(scan) = &plan {
96            // When predicates are pushed into a table scan, there is no input
97            // schema to resolve predicates against, so it must be handled specially
98            //
99            // Note that this is not `plan.schema()` which is the *output*
100            // schema, and reflects any pushed down projection. The output schema
101            // will not contain columns that *only* appear in pushed down predicates
102            // (and no where else) in the plan.
103            //
104            // Thus, use the full schema of the inner provider without any
105            // projection applied for simplification
106            Arc::new(DFSchema::try_from_qualified_schema(
107                scan.table_name.clone(),
108                &scan.source.schema(),
109            )?)
110        } else {
111            Arc::new(DFSchema::empty())
112        };
113
114        let info = SimplifyContext::builder()
115            .with_schema(schema)
116            .with_config_options(config.options())
117            .with_query_execution_start_time(config.query_execution_start_time())
118            .build();
119
120        // Inputs have already been rewritten (due to bottom-up traversal handled by Optimizer)
121        // Just need to rewrite our own expressions
122
123        let simplifier = ExprSimplifier::new(info);
124
125        // The left and right expressions in a Join on clause are not
126        // commutative, for reasons that are not entirely clear. Thus, do not
127        // reorder expressions in Join while simplifying.
128        //
129        // This is likely related to the fact that order of the columns must
130        // match the order of the children. see
131        // https://github.com/apache/datafusion/pull/8780 for more details
132        let simplifier = if let LogicalPlan::Join(_) = plan {
133            simplifier.with_canonicalize(false)
134        } else {
135            simplifier
136        };
137
138        // Preserve expression names to avoid changing the schema of the plan.
139        let name_preserver = NamePreserver::new(&plan);
140        let mut rewrite_expr = |expr: Expr| {
141            let name = name_preserver.save(&expr);
142            let expr = simplifier.simplify_with_cycle_count_transformed(expr)?.0;
143            Ok(Transformed::new_transformed(
144                name.restore(expr.data),
145                expr.transformed,
146            ))
147        };
148
149        plan.map_expressions(|expr| {
150            // Preserve the aliasing of grouping sets.
151            if let Expr::GroupingSet(_) = &expr {
152                expr.map_children(&mut rewrite_expr)
153            } else {
154                rewrite_expr(expr)
155            }
156        })?
157        .transform_data(rewrite_aggregate_non_aggregate_aggr_expr)
158    }
159}
160
161impl SimplifyExpressions {
162    #[expect(missing_docs)]
163    pub fn new() -> Self {
164        Self {}
165    }
166}
167
168/// Ensures that `LogicalPlan::Aggregate` is well formed after rewrites
169/// by potentially introducing an extra `Projection`.
170///
171/// Also applies the [`rewrite_multiple_linear_aggregates`] special case
172///
173/// # Rationale:
174///
175/// [`LogicalPlan::Aggregate`] requires agg expressions to be (possibly aliased)
176/// [`Expr::AggregateFunction`]. Some UDAF simplifiers may return other [`Expr`]
177/// variants.
178///
179/// # Operation
180///
181/// Rewrites things like this (note that `exp1` is not an aggregate):
182/// * `Aggregate(group_expr, aggr_expr=[exp1 + agg(exp2)])`
183///
184/// into:
185/// * `Projection(exp1 + _X)`
186/// * `  Aggregate(group_expr, aggr_expr=[agg(exp2) AS _X])`
187fn rewrite_aggregate_non_aggregate_aggr_expr(
188    plan: LogicalPlan,
189) -> Result<Transformed<LogicalPlan>> {
190    let LogicalPlan::Aggregate(Aggregate {
191        input,
192        group_expr,
193        mut aggr_expr,
194        schema,
195        ..
196    }) = plan
197    else {
198        return Ok(Transformed::no(plan));
199    };
200
201    let rewrote_aggs = rewrite_multiple_linear_aggregates(&mut aggr_expr)?;
202
203    // Ensure that all Aggregate arguments are AggregateExpr
204    if aggr_expr.iter().all(is_top_level_aggregate_expr) {
205        let new_plan = LogicalPlan::Aggregate(Aggregate::try_new_with_schema(
206            input, group_expr, aggr_expr, schema,
207        )?);
208        return if !rewrote_aggs {
209            Ok(Transformed::no(new_plan))
210        } else {
211            Ok(Transformed::yes(new_plan))
212        };
213    }
214
215    // Otherwise we need to add a Projection above Aggregate to calculate
216    // the final output expressions.
217
218    let inner_aggr_expr = find_aggregate_exprs(aggr_expr.iter());
219    let inner_aggregate = LogicalPlan::Aggregate(Aggregate::try_new(
220        Arc::clone(&input),
221        group_expr.clone(),
222        inner_aggr_expr,
223    )?);
224    let inner_aggregate = Arc::new(inner_aggregate);
225
226    let mut projection_exprs = aggregate_output_exprs(&group_expr)?;
227    projection_exprs.extend(aggr_expr);
228    let projection_exprs = projection_exprs
229        .into_iter()
230        .map(|expr| columnize_expr(expr, inner_aggregate.as_ref()))
231        .collect::<Result<Vec<_>>>()?;
232
233    Ok(Transformed::yes(LogicalPlan::Projection(
234        Projection::try_new(projection_exprs, inner_aggregate)?,
235    )))
236}
237
238fn is_top_level_aggregate_expr(expr: &Expr) -> bool {
239    matches!(
240        expr.clone().unalias_nested().data,
241        Expr::AggregateFunction(_)
242    )
243}
244
245fn aggregate_output_exprs(group_expr: &[Expr]) -> Result<Vec<Expr>> {
246    let mut output_exprs = grouping_set_to_exprlist(group_expr)?
247        .into_iter()
248        .cloned()
249        .collect::<Vec<_>>();
250
251    if matches!(group_expr, [Expr::GroupingSet(_)]) {
252        output_exprs.push(Expr::Column(Column::from_name(
253            Aggregate::INTERNAL_GROUPING_ID,
254        )));
255    }
256
257    Ok(output_exprs)
258}
259
260#[cfg(test)]
261mod tests {
262    use std::ops::Not;
263
264    use arrow::datatypes::{DataType, Field, Schema};
265    use chrono::{DateTime, Utc};
266
267    use datafusion_common::ScalarValue;
268    use datafusion_expr::logical_plan::builder::table_scan_with_filters;
269    use datafusion_expr::logical_plan::table_scan;
270    use datafusion_expr::*;
271    use datafusion_functions_aggregate::expr_fn::{max, min, sum};
272
273    use crate::OptimizerContext;
274    use crate::assert_optimized_plan_eq_snapshot;
275    use crate::test::{assert_fields_eq, test_table_scan_with_name};
276
277    use super::*;
278
279    fn test_table_scan() -> LogicalPlan {
280        let schema = Schema::new(vec![
281            Field::new("a", DataType::Boolean, false),
282            Field::new("b", DataType::Boolean, false),
283            Field::new("c", DataType::Boolean, false),
284            Field::new("d", DataType::UInt32, false),
285            Field::new("e", DataType::UInt32, true),
286        ]);
287        table_scan(Some("test"), &schema, None)
288            .expect("creating scan")
289            .build()
290            .expect("building plan")
291    }
292
293    macro_rules! assert_optimized_plan_equal {
294        (
295            $plan:expr,
296            @ $expected:literal $(,)?
297        ) => {{
298            let rules: Vec<Arc<dyn crate::OptimizerRule + Send + Sync>> = vec![Arc::new(SimplifyExpressions::new())];
299            let optimizer_ctx = OptimizerContext::new();
300            assert_optimized_plan_eq_snapshot!(
301                optimizer_ctx,
302                rules,
303                $plan,
304                @ $expected,
305            )
306        }};
307    }
308
309    #[test]
310    fn test_simplify_table_full_filter_in_scan() -> Result<()> {
311        let fields = vec![
312            Field::new("a", DataType::UInt32, false),
313            Field::new("b", DataType::UInt32, false),
314            Field::new("c", DataType::UInt32, false),
315        ];
316
317        let schema = Schema::new(fields);
318
319        let table_scan = table_scan_with_filters(
320            Some("test"),
321            &schema,
322            Some(vec![0]),
323            vec![col("b").is_not_null()],
324        )?
325        .build()?;
326        assert_eq!(1, table_scan.schema().fields().len());
327        assert_fields_eq(&table_scan, vec!["a"]);
328
329        assert_optimized_plan_equal!(
330            table_scan,
331            @ "TableScan: test projection=[a], full_filters=[Boolean(true)]"
332        )
333    }
334
335    #[test]
336    fn test_simplify_filter_pushdown() -> Result<()> {
337        let table_scan = test_table_scan();
338        let plan = LogicalPlanBuilder::from(table_scan)
339            .project(vec![col("a")])?
340            .filter(and(col("b").gt(lit(1)), col("b").gt(lit(1))))?
341            .build()?;
342
343        assert_optimized_plan_equal!(
344            plan,
345            @ r"
346        Filter: test.b > Int32(1)
347          Projection: test.a
348            TableScan: test
349        "
350        )
351    }
352
353    #[test]
354    fn test_simplify_optimized_plan() -> Result<()> {
355        let table_scan = test_table_scan();
356        let plan = LogicalPlanBuilder::from(table_scan)
357            .project(vec![col("a")])?
358            .filter(and(col("b").gt(lit(1)), col("b").gt(lit(1))))?
359            .build()?;
360
361        assert_optimized_plan_equal!(
362            plan,
363            @ r"
364        Filter: test.b > Int32(1)
365          Projection: test.a
366            TableScan: test
367        "
368        )
369    }
370
371    #[test]
372    fn test_simplify_udaf_to_non_aggregate_expr() -> Result<()> {
373        let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
374        let table_scan = table_scan(Some("test"), &schema, None)?
375            .build()
376            .expect("building scan");
377
378        let plan = LogicalPlanBuilder::from(table_scan)
379            .aggregate(Vec::<Expr>::new(), vec![sum(col("a") + lit(2i64))])?
380            .build()?;
381
382        assert_optimized_plan_equal!(
383            plan,
384            @r"
385        Aggregate: groupBy=[[]], aggr=[[sum(test.a + Int64(2))]]
386          TableScan: test
387        "
388        )?;
389        Ok(())
390    }
391
392    #[test]
393    fn test_simplify_common_sum_arg() -> Result<()> {
394        let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
395        let table_scan = table_scan(Some("test"), &schema, None)?
396            .build()
397            .expect("building scan");
398
399        let plan = LogicalPlanBuilder::from(table_scan)
400            .aggregate(
401                Vec::<Expr>::new(),
402                vec![sum(col("a") + lit(2i64)), sum(col("a") + lit(3i64))],
403            )?
404            .build()?;
405
406        assert_optimized_plan_equal!(
407            plan,
408            @r"
409        Projection: sum(test.a) + Int64(2) * CAST(count(test.a) AS Int64) AS sum(test.a + Int64(2)), sum(test.a) + Int64(3) * CAST(count(test.a) AS Int64) AS sum(test.a + Int64(3))
410          Aggregate: groupBy=[[]], aggr=[[sum(test.a), count(test.a)]]
411            TableScan: test
412        "
413        )?;
414        Ok(())
415    }
416
417    #[test]
418    fn test_simplify_optimized_plan_with_or() -> Result<()> {
419        let table_scan = test_table_scan();
420        let plan = LogicalPlanBuilder::from(table_scan)
421            .project(vec![col("a")])?
422            .filter(or(col("b").gt(lit(1)), col("b").gt(lit(1))))?
423            .build()?;
424
425        assert_optimized_plan_equal!(
426            plan,
427            @ r"
428        Filter: test.b > Int32(1)
429          Projection: test.a
430            TableScan: test
431        "
432        )
433    }
434
435    #[test]
436    fn test_simplify_optimized_plan_with_composed_and() -> Result<()> {
437        let table_scan = test_table_scan();
438        // ((c > 5) AND (d < 6)) AND (c > 5) --> (c > 5) AND (d < 6)
439        let plan = LogicalPlanBuilder::from(table_scan)
440            .project(vec![col("a"), col("b")])?
441            .filter(and(
442                and(col("a").gt(lit(5)), col("b").lt(lit(6))),
443                col("a").gt(lit(5)),
444            ))?
445            .build()?;
446
447        assert_optimized_plan_equal!(
448            plan,
449            @ r"
450        Filter: test.a > Int32(5) AND test.b < Int32(6)
451          Projection: test.a, test.b
452            TableScan: test
453        "
454        )
455    }
456
457    #[test]
458    fn test_simplify_optimized_plan_eq_expr() -> Result<()> {
459        let table_scan = test_table_scan();
460        let plan = LogicalPlanBuilder::from(table_scan)
461            .filter(col("b").eq(lit(true)))?
462            .filter(col("c").eq(lit(false)))?
463            .project(vec![col("a")])?
464            .build()?;
465
466        assert_optimized_plan_equal!(
467            plan,
468            @ r"
469        Projection: test.a
470          Filter: NOT test.c
471            Filter: test.b
472              TableScan: test
473        "
474        )
475    }
476
477    #[test]
478    fn test_simplify_optimized_plan_not_eq_expr() -> Result<()> {
479        let table_scan = test_table_scan();
480        let plan = LogicalPlanBuilder::from(table_scan)
481            .filter(col("b").not_eq(lit(true)))?
482            .filter(col("c").not_eq(lit(false)))?
483            .limit(0, Some(1))?
484            .project(vec![col("a")])?
485            .build()?;
486
487        assert_optimized_plan_equal!(
488            plan,
489            @ r"
490        Projection: test.a
491          Limit: skip=0, fetch=1
492            Filter: test.c
493              Filter: NOT test.b
494                TableScan: test
495        "
496        )
497    }
498
499    #[test]
500    fn test_simplify_optimized_plan_and_expr() -> Result<()> {
501        let table_scan = test_table_scan();
502        let plan = LogicalPlanBuilder::from(table_scan)
503            .filter(col("b").not_eq(lit(true)).and(col("c").eq(lit(true))))?
504            .project(vec![col("a")])?
505            .build()?;
506
507        assert_optimized_plan_equal!(
508            plan,
509            @ r"
510        Projection: test.a
511          Filter: NOT test.b AND test.c
512            TableScan: test
513        "
514        )
515    }
516
517    #[test]
518    fn test_simplify_optimized_plan_or_expr() -> Result<()> {
519        let table_scan = test_table_scan();
520        let plan = LogicalPlanBuilder::from(table_scan)
521            .filter(col("b").not_eq(lit(true)).or(col("c").eq(lit(false))))?
522            .project(vec![col("a")])?
523            .build()?;
524
525        assert_optimized_plan_equal!(
526            plan,
527            @ r"
528        Projection: test.a
529          Filter: NOT test.b OR NOT test.c
530            TableScan: test
531        "
532        )
533    }
534
535    #[test]
536    fn test_simplify_optimized_plan_not_expr() -> Result<()> {
537        let table_scan = test_table_scan();
538        let plan = LogicalPlanBuilder::from(table_scan)
539            .filter(col("b").eq(lit(false)).not())?
540            .project(vec![col("a")])?
541            .build()?;
542
543        assert_optimized_plan_equal!(
544            plan,
545            @ r"
546        Projection: test.a
547          Filter: test.b
548            TableScan: test
549        "
550        )
551    }
552
553    #[test]
554    fn test_simplify_optimized_plan_support_projection() -> Result<()> {
555        let table_scan = test_table_scan();
556        let plan = LogicalPlanBuilder::from(table_scan)
557            .project(vec![col("a"), col("d"), col("b").eq(lit(false))])?
558            .build()?;
559
560        assert_optimized_plan_equal!(
561            plan,
562            @ r"
563        Projection: test.a, test.d, NOT test.b AS test.b = Boolean(false)
564          TableScan: test
565        "
566        )
567    }
568
569    #[test]
570    fn test_simplify_optimized_plan_support_aggregate() -> Result<()> {
571        let table_scan = test_table_scan();
572        let plan = LogicalPlanBuilder::from(table_scan)
573            .project(vec![col("a"), col("c"), col("b")])?
574            .aggregate(
575                vec![col("a"), col("c")],
576                vec![max(col("b").eq(lit(true))), min(col("b"))],
577            )?
578            .build()?;
579
580        assert_optimized_plan_equal!(
581            plan,
582            @ r"
583        Aggregate: groupBy=[[test.a, test.c]], aggr=[[max(test.b) AS max(test.b = Boolean(true)), min(test.b)]]
584          Projection: test.a, test.c, test.b
585            TableScan: test
586        "
587        )
588    }
589
590    #[test]
591    fn test_simplify_optimized_plan_support_values() -> Result<()> {
592        let expr1 = Expr::BinaryExpr(BinaryExpr::new(
593            Box::new(lit(1)),
594            Operator::Plus,
595            Box::new(lit(2)),
596        ));
597        let expr2 = Expr::BinaryExpr(BinaryExpr::new(
598            Box::new(lit(2)),
599            Operator::Minus,
600            Box::new(lit(1)),
601        ));
602        let values = vec![vec![expr1, expr2]];
603        let plan = LogicalPlanBuilder::values(values)?.build()?;
604
605        assert_optimized_plan_equal!(
606            plan,
607            @ "Values: (Int32(3) AS Int32(1) + Int32(2), Int32(1) AS Int32(2) - Int32(1))"
608        )
609    }
610
611    fn get_optimized_plan_formatted(
612        plan: LogicalPlan,
613        date_time: &DateTime<Utc>,
614    ) -> String {
615        let config = OptimizerContext::new().with_query_execution_start_time(*date_time);
616        let rule = SimplifyExpressions::new();
617
618        let optimized_plan = rule.rewrite(plan, &config).unwrap().data;
619        format!("{optimized_plan}")
620    }
621
622    #[test]
623    fn cast_expr() -> Result<()> {
624        let table_scan = test_table_scan();
625        let proj = vec![Expr::Cast(Cast::new(Box::new(lit("0")), DataType::Int32))];
626        let plan = LogicalPlanBuilder::from(table_scan)
627            .project(proj)?
628            .build()?;
629
630        let expected = "Projection: Int32(0) AS Utf8(\"0\")\
631            \n  TableScan: test";
632        let actual = get_optimized_plan_formatted(plan, &Utc::now());
633        assert_eq!(expected, actual);
634        Ok(())
635    }
636
637    #[test]
638    fn simplify_and_eval() -> Result<()> {
639        // demonstrate a case where the evaluation needs to run prior
640        // to the simplifier for it to work
641        let table_scan = test_table_scan();
642        let time = Utc::now();
643        // (true or false) != col --> !col
644        let proj = vec![lit(true).or(lit(false)).not_eq(col("a"))];
645        let plan = LogicalPlanBuilder::from(table_scan)
646            .project(proj)?
647            .build()?;
648
649        let actual = get_optimized_plan_formatted(plan, &time);
650        let expected = "Projection: NOT test.a AS Boolean(true) OR Boolean(false) != test.a\
651                        \n  TableScan: test";
652
653        assert_eq!(expected, actual);
654        Ok(())
655    }
656
657    #[test]
658    fn simplify_not_binary() -> Result<()> {
659        let table_scan = test_table_scan();
660
661        let plan = LogicalPlanBuilder::from(table_scan)
662            .filter(col("d").gt(lit(10)).not())?
663            .build()?;
664
665        assert_optimized_plan_equal!(
666            plan,
667            @ r"
668        Filter: test.d <= Int32(10)
669          TableScan: test
670        "
671        )
672    }
673
674    #[test]
675    fn simplify_not_bool_and() -> Result<()> {
676        let table_scan = test_table_scan();
677
678        let plan = LogicalPlanBuilder::from(table_scan)
679            .filter(col("d").gt(lit(10)).and(col("d").lt(lit(100))).not())?
680            .build()?;
681
682        assert_optimized_plan_equal!(
683            plan,
684            @ r"
685        Filter: test.d <= Int32(10) OR test.d >= Int32(100)
686          TableScan: test
687        "
688        )
689    }
690
691    #[test]
692    fn simplify_not_bool_or() -> Result<()> {
693        let table_scan = test_table_scan();
694
695        let plan = LogicalPlanBuilder::from(table_scan)
696            .filter(col("d").gt(lit(10)).or(col("d").lt(lit(100))).not())?
697            .build()?;
698
699        assert_optimized_plan_equal!(
700            plan,
701            @ r"
702        Filter: test.d <= Int32(10) AND test.d >= Int32(100)
703          TableScan: test
704        "
705        )
706    }
707
708    #[test]
709    fn simplify_not_not() -> Result<()> {
710        let table_scan = test_table_scan();
711
712        let plan = LogicalPlanBuilder::from(table_scan)
713            .filter(col("d").gt(lit(10)).not().not())?
714            .build()?;
715
716        assert_optimized_plan_equal!(
717            plan,
718            @ r"
719        Filter: test.d > Int32(10)
720          TableScan: test
721        "
722        )
723    }
724
725    #[test]
726    fn simplify_not_null() -> Result<()> {
727        let table_scan = test_table_scan();
728
729        let plan = LogicalPlanBuilder::from(table_scan)
730            .filter(col("e").is_null().not())?
731            .build()?;
732
733        assert_optimized_plan_equal!(
734            plan,
735            @ r"
736        Filter: test.e IS NOT NULL
737          TableScan: test
738        "
739        )
740    }
741
742    #[test]
743    fn simplify_not_not_null() -> Result<()> {
744        let table_scan = test_table_scan();
745
746        let plan = LogicalPlanBuilder::from(table_scan)
747            .filter(col("e").is_not_null().not())?
748            .build()?;
749
750        assert_optimized_plan_equal!(
751            plan,
752            @ r"
753        Filter: test.e IS NULL
754          TableScan: test
755        "
756        )
757    }
758
759    #[test]
760    fn simplify_not_in() -> Result<()> {
761        let table_scan = test_table_scan();
762
763        let plan = LogicalPlanBuilder::from(table_scan)
764            .filter(col("d").in_list(vec![lit(1), lit(2), lit(3)], false).not())?
765            .build()?;
766
767        assert_optimized_plan_equal!(
768            plan,
769            @ r"
770        Filter: test.d != Int32(1) AND test.d != Int32(2) AND test.d != Int32(3)
771          TableScan: test
772        "
773        )
774    }
775
776    #[test]
777    fn simplify_not_not_in() -> Result<()> {
778        let table_scan = test_table_scan();
779
780        let plan = LogicalPlanBuilder::from(table_scan)
781            .filter(col("d").in_list(vec![lit(1), lit(2), lit(3)], true).not())?
782            .build()?;
783
784        assert_optimized_plan_equal!(
785            plan,
786            @ r"
787        Filter: test.d = Int32(1) OR test.d = Int32(2) OR test.d = Int32(3)
788          TableScan: test
789        "
790        )
791    }
792
793    #[test]
794    fn simplify_not_between() -> Result<()> {
795        let table_scan = test_table_scan();
796        let qual = col("d").between(lit(1), lit(10));
797
798        let plan = LogicalPlanBuilder::from(table_scan)
799            .filter(qual.not())?
800            .build()?;
801
802        assert_optimized_plan_equal!(
803            plan,
804            @ r"
805        Filter: test.d < Int32(1) OR test.d > Int32(10)
806          TableScan: test
807        "
808        )
809    }
810
811    #[test]
812    fn simplify_not_not_between() -> Result<()> {
813        let table_scan = test_table_scan();
814        let qual = col("d").not_between(lit(1), lit(10));
815
816        let plan = LogicalPlanBuilder::from(table_scan)
817            .filter(qual.not())?
818            .build()?;
819
820        assert_optimized_plan_equal!(
821            plan,
822            @ r"
823        Filter: test.d >= Int32(1) AND test.d <= Int32(10)
824          TableScan: test
825        "
826        )
827    }
828
829    #[test]
830    fn simplify_not_like() -> Result<()> {
831        let schema = Schema::new(vec![
832            Field::new("a", DataType::Utf8, false),
833            Field::new("b", DataType::Utf8, false),
834        ]);
835        let table_scan = table_scan(Some("test"), &schema, None)
836            .expect("creating scan")
837            .build()
838            .expect("building plan");
839
840        let plan = LogicalPlanBuilder::from(table_scan)
841            .filter(col("a").like(col("b")).not())?
842            .build()?;
843
844        assert_optimized_plan_equal!(
845            plan,
846            @ r"
847        Filter: test.a NOT LIKE test.b
848          TableScan: test
849        "
850        )
851    }
852
853    #[test]
854    fn simplify_not_not_like() -> Result<()> {
855        let schema = Schema::new(vec![
856            Field::new("a", DataType::Utf8, false),
857            Field::new("b", DataType::Utf8, false),
858        ]);
859        let table_scan = table_scan(Some("test"), &schema, None)
860            .expect("creating scan")
861            .build()
862            .expect("building plan");
863
864        let plan = LogicalPlanBuilder::from(table_scan)
865            .filter(col("a").not_like(col("b")).not())?
866            .build()?;
867
868        assert_optimized_plan_equal!(
869            plan,
870            @ r"
871        Filter: test.a LIKE test.b
872          TableScan: test
873        "
874        )
875    }
876
877    #[test]
878    fn simplify_not_ilike() -> Result<()> {
879        let schema = Schema::new(vec![
880            Field::new("a", DataType::Utf8, false),
881            Field::new("b", DataType::Utf8, false),
882        ]);
883        let table_scan = table_scan(Some("test"), &schema, None)
884            .expect("creating scan")
885            .build()
886            .expect("building plan");
887
888        let plan = LogicalPlanBuilder::from(table_scan)
889            .filter(col("a").ilike(col("b")).not())?
890            .build()?;
891
892        assert_optimized_plan_equal!(
893            plan,
894            @ r"
895        Filter: test.a NOT ILIKE test.b
896          TableScan: test
897        "
898        )
899    }
900
901    #[test]
902    fn simplify_not_distinct_from() -> Result<()> {
903        let table_scan = test_table_scan();
904
905        let plan = LogicalPlanBuilder::from(table_scan)
906            .filter(binary_expr(col("d"), Operator::IsDistinctFrom, lit(10)).not())?
907            .build()?;
908
909        assert_optimized_plan_equal!(
910            plan,
911            @ r"
912        Filter: test.d IS NOT DISTINCT FROM Int32(10)
913          TableScan: test
914        "
915        )
916    }
917
918    #[test]
919    fn simplify_not_not_distinct_from() -> Result<()> {
920        let table_scan = test_table_scan();
921
922        let plan = LogicalPlanBuilder::from(table_scan)
923            .filter(binary_expr(col("d"), Operator::IsNotDistinctFrom, lit(10)).not())?
924            .build()?;
925
926        assert_optimized_plan_equal!(
927            plan,
928            @ r"
929        Filter: test.d IS DISTINCT FROM Int32(10)
930          TableScan: test
931        "
932        )
933    }
934
935    #[test]
936    fn simplify_equijoin_predicate() -> Result<()> {
937        let t1 = test_table_scan_with_name("t1")?;
938        let t2 = test_table_scan_with_name("t2")?;
939
940        let left_key = col("t1.a") + lit(1i64).cast_to(&DataType::UInt32, t1.schema())?;
941        let right_key =
942            col("t2.a") + lit(2i64).cast_to(&DataType::UInt32, t2.schema())?;
943        let plan = LogicalPlanBuilder::from(t1)
944            .join_with_expr_keys(
945                t2,
946                JoinType::Inner,
947                (vec![left_key], vec![right_key]),
948                None,
949            )?
950            .build()?;
951
952        // before simplify: t1.a + CAST(Int64(1), UInt32) = t2.a + CAST(Int64(2), UInt32)
953        // after simplify: t1.a + UInt32(1) = t2.a + UInt32(2) AS t1.a + Int64(1) = t2.a + Int64(2)
954        assert_optimized_plan_equal!(
955            plan,
956            @ r"
957        Inner Join: t1.a + UInt32(1) = t2.a + UInt32(2)
958          TableScan: t1
959          TableScan: t2
960        "
961        )
962    }
963
964    #[test]
965    fn simplify_is_not_null() -> Result<()> {
966        let table_scan = test_table_scan();
967
968        let plan = LogicalPlanBuilder::from(table_scan)
969            .filter(col("d").is_not_null())?
970            .build()?;
971
972        assert_optimized_plan_equal!(
973            plan,
974            @ r"
975        Filter: Boolean(true)
976          TableScan: test
977        "
978        )
979    }
980
981    #[test]
982    fn simplify_is_null() -> Result<()> {
983        let table_scan = test_table_scan();
984
985        let plan = LogicalPlanBuilder::from(table_scan)
986            .filter(col("d").is_null())?
987            .build()?;
988
989        assert_optimized_plan_equal!(
990            plan,
991            @ r"
992        Filter: Boolean(false)
993          TableScan: test
994        "
995        )
996    }
997
998    #[test]
999    fn simplify_grouping_sets() -> Result<()> {
1000        let table_scan = test_table_scan();
1001        let plan = LogicalPlanBuilder::from(table_scan)
1002            .aggregate(
1003                [grouping_set(vec![
1004                    vec![(lit(42).alias("prev") + lit(1)).alias("age"), col("a")],
1005                    vec![col("a").or(col("b")).and(lit(1).lt(lit(0))).alias("cond")],
1006                    vec![col("d").alias("e"), (lit(1) + lit(2))],
1007                ])],
1008                [] as [Expr; 0],
1009            )?
1010            .build()?;
1011
1012        assert_optimized_plan_equal!(
1013            plan,
1014            @ r"
1015        Aggregate: groupBy=[[GROUPING SETS ((Int32(43) AS age, test.a), (Boolean(false) AS cond), (test.d AS e, Int32(3) AS Int32(1) + Int32(2)))]], aggr=[[]]
1016          TableScan: test
1017        "
1018        )
1019    }
1020
1021    #[test]
1022    fn test_simplify_regex_special_cases() -> Result<()> {
1023        let schema = Schema::new(vec![
1024            Field::new("a", DataType::Utf8, true),
1025            Field::new("b", DataType::Utf8, false),
1026        ]);
1027        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1028
1029        // Test `~ ".*"` transforms to true for any non-NULL string
1030        let plan = LogicalPlanBuilder::from(table_scan.clone())
1031            .filter(binary_expr(col("a"), Operator::RegexMatch, lit(".*")))?
1032            .build()?;
1033
1034        assert_optimized_plan_equal!(
1035            plan,
1036            @ r"
1037        Filter: test.a IS NOT NULL
1038          TableScan: test
1039        "
1040        )?;
1041
1042        // Test `!~ ".*"` preserves NULL semantics while remaining false for non-NULL strings
1043        let plan = LogicalPlanBuilder::from(table_scan.clone())
1044            .filter(binary_expr(col("a"), Operator::RegexNotMatch, lit(".*")))?
1045            .build()?;
1046
1047        assert_optimized_plan_equal!(
1048            plan,
1049            @ r"
1050        Filter: test.a IS NULL AND Boolean(NULL)
1051          TableScan: test
1052        "
1053        )?;
1054
1055        // Test case-insensitive versions
1056
1057        // Test `~* ".*"` transforms to true for any non-NULL string
1058        let plan = LogicalPlanBuilder::from(table_scan.clone())
1059            .filter(binary_expr(col("b"), Operator::RegexIMatch, lit(".*")))?
1060            .build()?;
1061
1062        assert_optimized_plan_equal!(
1063            plan,
1064            @ r"
1065        Filter: Boolean(true)
1066          TableScan: test
1067        "
1068        )?;
1069
1070        // Test NULL `!~ ".*"` transforms to Boolean(NULL)
1071        let plan = LogicalPlanBuilder::from(table_scan.clone())
1072            .filter(binary_expr(
1073                lit(ScalarValue::Utf8(None)),
1074                Operator::RegexNotMatch,
1075                lit(".*"),
1076            ))?
1077            .build()?;
1078
1079        assert_optimized_plan_equal!(
1080            plan,
1081            @ r"
1082        Filter: Boolean(NULL)
1083          TableScan: test
1084        "
1085        )?;
1086
1087        // Test `!~* ".*"` preserves NULL semantics while remaining false for non-NULL strings
1088        let plan = LogicalPlanBuilder::from(table_scan.clone())
1089            .filter(binary_expr(col("a"), Operator::RegexNotIMatch, lit(".*")))?
1090            .build()?;
1091
1092        assert_optimized_plan_equal!(
1093            plan,
1094            @ r"
1095        Filter: test.a IS NULL AND Boolean(NULL)
1096          TableScan: test
1097        "
1098        )?;
1099
1100        // Test NULL `!~* ".*"` transforms to Boolean(NULL)
1101        let plan = LogicalPlanBuilder::from(table_scan.clone())
1102            .filter(binary_expr(
1103                lit(ScalarValue::Utf8(None)),
1104                Operator::RegexNotIMatch,
1105                lit(".*"),
1106            ))?
1107            .build()?;
1108
1109        assert_optimized_plan_equal!(
1110            plan,
1111            @ r"
1112        Filter: Boolean(NULL)
1113          TableScan: test
1114        "
1115        )
1116    }
1117
1118    #[test]
1119    fn simplify_not_in_list() -> Result<()> {
1120        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
1121        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1122
1123        let plan = LogicalPlanBuilder::from(table_scan)
1124            .filter(col("a").in_list(vec![lit("a"), lit("b")], false).not())?
1125            .build()?;
1126
1127        assert_optimized_plan_equal!(
1128            plan,
1129            @ r#"
1130        Filter: test.a != Utf8("a") AND test.a != Utf8("b")
1131          TableScan: test
1132        "#
1133        )
1134    }
1135
1136    #[test]
1137    fn simplify_not_not_in_list() -> Result<()> {
1138        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
1139        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1140
1141        let plan = LogicalPlanBuilder::from(table_scan)
1142            .filter(
1143                col("a")
1144                    .in_list(vec![lit("a"), lit("b")], false)
1145                    .not()
1146                    .not(),
1147            )?
1148            .build()?;
1149
1150        assert_optimized_plan_equal!(
1151            plan,
1152            @ r#"
1153        Filter: test.a = Utf8("a") OR test.a = Utf8("b")
1154          TableScan: test
1155        "#
1156        )
1157    }
1158
1159    #[test]
1160    fn simplify_not_exists() -> Result<()> {
1161        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
1162        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1163        let table_scan2 =
1164            datafusion_expr::table_scan(Some("test2"), &schema, None)?.build()?;
1165
1166        let plan = LogicalPlanBuilder::from(table_scan)
1167            .filter(
1168                exists(Arc::new(LogicalPlanBuilder::from(table_scan2).build()?)).not(),
1169            )?
1170            .build()?;
1171
1172        assert_optimized_plan_equal!(
1173            plan,
1174            @ r"
1175        Filter: NOT EXISTS (<subquery>)
1176          Subquery:
1177            TableScan: test2
1178          TableScan: test
1179        "
1180        )
1181    }
1182
1183    #[test]
1184    fn simplify_not_not_exists() -> Result<()> {
1185        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
1186        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1187        let table_scan2 =
1188            datafusion_expr::table_scan(Some("test2"), &schema, None)?.build()?;
1189
1190        let plan = LogicalPlanBuilder::from(table_scan)
1191            .filter(
1192                exists(Arc::new(LogicalPlanBuilder::from(table_scan2).build()?))
1193                    .not()
1194                    .not(),
1195            )?
1196            .build()?;
1197
1198        assert_optimized_plan_equal!(
1199            plan,
1200            @ r"
1201        Filter: EXISTS (<subquery>)
1202          Subquery:
1203            TableScan: test2
1204          TableScan: test
1205        "
1206        )
1207    }
1208
1209    #[test]
1210    fn simplify_not_in_subquery() -> Result<()> {
1211        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
1212        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1213        let table_scan2 =
1214            datafusion_expr::table_scan(Some("test2"), &schema, None)?.build()?;
1215
1216        let plan = LogicalPlanBuilder::from(table_scan)
1217            .filter(
1218                in_subquery(
1219                    col("a"),
1220                    Arc::new(LogicalPlanBuilder::from(table_scan2).build()?),
1221                )
1222                .not(),
1223            )?
1224            .build()?;
1225
1226        assert_optimized_plan_equal!(
1227            plan,
1228            @ r"
1229        Filter: test.a NOT IN (<subquery>)
1230          Subquery:
1231            TableScan: test2
1232          TableScan: test
1233        "
1234        )
1235    }
1236
1237    #[test]
1238    fn simplify_not_not_in_subquery() -> Result<()> {
1239        let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]);
1240        let table_scan = table_scan(Some("test"), &schema, None)?.build()?;
1241        let table_scan2 =
1242            datafusion_expr::table_scan(Some("test2"), &schema, None)?.build()?;
1243
1244        let plan = LogicalPlanBuilder::from(table_scan)
1245            .filter(
1246                in_subquery(
1247                    col("a"),
1248                    Arc::new(LogicalPlanBuilder::from(table_scan2).build()?),
1249                )
1250                .not()
1251                .not(),
1252            )?
1253            .build()?;
1254
1255        assert_optimized_plan_equal!(
1256            plan,
1257            @ r"
1258        Filter: test.a IN (<subquery>)
1259          Subquery:
1260            TableScan: test2
1261          TableScan: test
1262        "
1263        )
1264    }
1265}