This is an automated email from the ASF dual-hosted git repository.

xuzifu666 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 02cfb90482 [CALCITE-7657] Apply the absorption law to simplify boolean 
expressions
02cfb90482 is described below

commit 02cfb904822e32e39447ed13ba0cba9618203b54
Author: Yu Xu <[email protected]>
AuthorDate: Thu Jul 16 10:06:30 2026 +0800

    [CALCITE-7657] Apply the absorption law to simplify boolean expressions
---
 .../java/org/apache/calcite/rex/RexSimplify.java   | 49 +++++++++++++++
 .../org/apache/calcite/rex/RexProgramTest.java     | 48 +++++++++++++--
 .../org/apache/calcite/test/JdbcAdapterTest.java   |  5 +-
 .../MaterializedViewSubstitutionVisitorTest.java   |  2 +-
 .../org/apache/calcite/test/RelBuilderTest.java    | 70 ++++++++++++++++++++++
 core/src/test/resources/sql/sub-query.iq           | 12 ++--
 6 files changed, 171 insertions(+), 15 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java 
b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
index 58979c0f5c..b275c8b6d9 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -93,6 +93,9 @@ public class RexSimplify {
 
   private static final Strong STRONG = new Strong();
 
+  /** Maximum number of terms for which to apply the absorption law. */
+  private static final int MAX_TERMS_FOR_ABSORPTION = 20;
+
   /**
    * Creates a RexSimplify.
    *
@@ -1843,6 +1846,9 @@ RexNode simplifyAnd2(List<RexNode> terms, List<RexNode> 
notTerms) {
                 SqlStdOperatorTable.IS_NULL, notSatisfiableNullable), 
UNKNOWN));
       }
     }
+    // Absorption law: a AND (a OR b) => a
+    absorb(terms, SqlKind.OR);
+
     // Add the NOT disjunctions back in.
     for (RexNode notDisjunction : notTerms) {
       terms.add(simplify(not(notDisjunction), UNKNOWN));
@@ -2087,6 +2093,9 @@ private <C extends Comparable<C>> RexNode 
simplifyAnd2ForUnknownAsFalse(
     if (!Collections.disjoint(nullOperands, strongOperands)) {
       return rexBuilder.makeLiteral(false);
     }
+    // Absorption law: a AND (a OR b) => a
+    absorb(terms, SqlKind.OR);
+
     // Remove not necessary IS NOT NULL expressions.
     // Example. IS NOT NULL(x) AND x < 5  : x < 5
     for (RexNode operand : notNullOperands) {
@@ -2367,9 +2376,49 @@ private RexNode simplifyOrs(List<RexNode> terms, 
RexUnknownAs unknownAs) {
         break;
       }
     }
+
+    // Absorption law: a OR (a AND b) => a
+    absorb(terms, SqlKind.AND);
+
     return RexUtil.composeDisjunction(rexBuilder, terms);
   }
 
+  /**
+   * Applies the absorption law to a list of terms, removing any composite term
+   * that is absorbed by a sibling term.
+   *
+   * <p>When {@code compositeKind} is {@link SqlKind#OR}, removes any
+   * {@code (a OR b)} term whose disjunctions contain a sibling {@code a}, so
+   * {@code a AND (a OR b) => a}. When it is {@link SqlKind#AND}, removes any
+   * {@code (a AND b)} term whose conjunctions contain a sibling {@code a}, so
+   * {@code a OR (a AND b) => a}.
+   *
+   * <p>The absorbing sibling {@code a} must be deterministic; otherwise its 
two
+   * occurrences might evaluate differently and the rewrite would not be
+   * equivalence-preserving.
+   */
+  private static void absorb(List<RexNode> terms, SqlKind compositeKind) {
+    if (terms.size() > MAX_TERMS_FOR_ABSORPTION) {
+      return;
+    }
+    for (int i = 0; i < terms.size(); i++) {
+      final RexNode term = terms.get(i);
+      if (term.getKind() == compositeKind) {
+        final List<RexNode> components = compositeKind == SqlKind.OR
+            ? RelOptUtil.disjunctions(term)
+            : RelOptUtil.conjunctions(term);
+        for (RexNode other : terms) {
+          if (other != term && components.contains(other)
+              && RexUtil.isDeterministic(other)) {
+            terms.remove(i);
+            i--;
+            break;
+          }
+        }
+      }
+    }
+  }
+
   private Pair<Comparable, RuntimeException> evaluate(RexNode e, Map<RexNode, 
Comparable> map) {
     Comparable c = null;
     RuntimeException ex = null;
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java 
b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
index bf506ceb12..5f8b8edfb1 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -513,6 +513,45 @@ private RexProgramBuilder createProg(int variant) {
         "false");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7657";>[CALCITE-7657]
+   * Apply the absorption law to simplify boolean expressions</a>. */
+  @Test void testAbsorptionLaw() {
+    // AND absorption: a AND (a OR b) => a
+    checkSimplify(and(vBool(), or(vBool(), vBool(1))), "?0.bool0");
+    checkSimplify(and(or(vBool(), vBool(1)), vBool()), "?0.bool0");
+
+    // OR absorption: a OR (a AND b) => a
+    checkSimplify(or(vBool(), and(vBool(), vBool(1))), "?0.bool0");
+    checkSimplify(or(and(vBool(), vBool(1)), vBool()), "?0.bool0");
+
+    // with not-null booleans
+    checkSimplify(and(vBoolNotNull(), or(vBoolNotNull(), vBoolNotNull(1))), 
"?0.notNullBool0");
+    checkSimplify(or(vBoolNotNull(), and(vBoolNotNull(), vBoolNotNull(1))), 
"?0.notNullBool0");
+
+    // filter mode (unknownAsFalse)
+    checkSimplifyFilter(and(vBool(), or(vBool(), vBool(1))), "?0.bool0");
+    checkSimplifyFilter(or(vBool(), and(vBool(), vBool(1))), "?0.bool0");
+  }
+
+  @Test void testAbsorptionLawWithNonDeterministic() {
+    // a is a non-deterministic boolean ("NDC()")
+    final SqlOperator ndc = getNoDeterministicOperator();
+    final RexNode a = rexBuilder.makeCall(ndc);
+    final RexNode b = gt(vInt(1), literal(1));
+
+    // a AND (a OR b) must NOT be simplified to a
+    checkSimplifyUnchanged(and(a, or(a, b)));
+    // a OR (a AND b) must NOT be simplified to a
+    checkSimplifyUnchanged(or(a, and(a, b)));
+
+    // Sanity check: when a is deterministic, absorption does apply.
+    final SqlOperator dc = getDeterministicOperator();
+    final RexNode da = rexBuilder.makeCall(dc);
+    checkSimplify(and(da, or(da, b)), "DC()");
+    checkSimplify(or(da, and(da, b)), "DC()");
+  }
+
   @Disabled("CALCITE-3457: AssertionError in RexSimplify.validateStrongPolicy")
   @Test void reproducerFor3457() {
     // Identified with RexProgramFuzzyTest#testFuzzy, seed=4887662474363391810L
@@ -3064,10 +3103,9 @@ trueLiteral, literal(1),
     // ==>
     // "A IS NOT NULL"
     SqlOperator dc = getDeterministicOperator();
-    checkSimplify2(
+    checkSimplify(
         and(or(isNotNull(rexBuilder.makeCall(dc)), gt(vInt(2), literal(2))),
             isNotNull(rexBuilder.makeCall(dc))),
-        "AND(OR(IS NOT NULL(DC()), >(?0.int2, 2)), IS NOT NULL(DC()))",
         "IS NOT NULL(DC())");
   }
 
@@ -3962,7 +4000,7 @@ private static String getString(Map<RexNode, RexNode> 
map) {
     //    -> "x = x AND y < y" (treating unknown as unknown)
     //    -> false (treating unknown as false)
     checkSimplify3(and(eq(vInt(1), vInt(1)), not(ge(vInt(2), vInt(2)))),
-        "AND(OR(null, IS NOT NULL(?0.int1)), null, IS NULL(?0.int2))",
+        "AND(null, IS NULL(?0.int2))",
         "false",
         "IS NULL(?0.int2)");
 
@@ -3970,7 +4008,7 @@ private static String getString(Map<RexNode, RexNode> 
map) {
     //   -> "OR(x <> x, y >= y)" (treating unknown as unknown)
     //   -> "y IS NOT NULL" (treating unknown as false)
     checkSimplify3(not(and(eq(vInt(1), vInt(1)), not(ge(vInt(2), vInt(2))))),
-        "OR(AND(null, IS NULL(?0.int1)), null, IS NOT NULL(?0.int2))",
+        "OR(null, IS NOT NULL(?0.int2))",
         "IS NOT NULL(?0.int2)",
         "true");
   }
@@ -4028,7 +4066,7 @@ private static String getString(Map<RexNode, RexNode> 
map) {
     //   -> "AND(x <> x, y >= y)" (treating unknown as unknown)
     //   -> "FALSE" (treating unknown as false)
     checkSimplify3(not(or(eq(vInt(1), vInt(1)), not(ge(vInt(2), vInt(2))))),
-        "AND(null, IS NULL(?0.int1), OR(null, IS NOT NULL(?0.int2)))",
+        "AND(null, IS NULL(?0.int1))",
         "false",
         "IS NULL(?0.int1)");
   }
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 5ecf63c5ba..fbf502169d 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -322,9 +322,8 @@ class JdbcAdapterTest {
             + "          JdbcProject($f2=[OR(IS NOT NULL($7), IS NOT 
NULL($1))])\n"
             + "            JdbcTableScan(table=[[SCOTT, EMP]])\n"
             + "    JdbcToEnumerableConverter\n"
-            + "      JdbcAggregate(group=[{0, 1}], i=[LITERAL_AGG(true)], 
em=[MAX($2)])\n"
-            + "        JdbcProject(DEPTNO=[$7], ENAME=[CAST($1):VARCHAR(14)],"
-            + " $f2=[AND(IS NOT NULL($7), IS NOT NULL($1))])\n"
+            + "      JdbcAggregate(group=[{0, 1}], i=[LITERAL_AGG(true)])\n"
+            + "        JdbcProject(DEPTNO=[$7], 
ENAME=[CAST($1):VARCHAR(14)])\n"
             + "          JdbcFilter(condition=[OR(IS NOT NULL($7), IS NOT 
NULL($1))])\n"
             + "            JdbcTableScan(table=[[SCOTT, EMP]])\n\n");
   }
diff --git 
a/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
 
b/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
index a19cd683d2..285c1cf9a3 100644
--- 
a/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
+++ 
b/core/src/test/java/org/apache/calcite/test/MaterializedViewSubstitutionVisitorTest.java
@@ -1653,7 +1653,7 @@ protected final MaterializedViewFixture sql(String 
materialize,
                         SqlStdOperatorTable.NOT,
                         i4))));
     f.checkSatisfiable(e8,
-        "AND(=($0, 0), $2, $3, OR(NOT($2), NOT($3), NOT($4)), NOT($4))");
+        "AND(=($0, 0), $2, $3, NOT($4))");
   }
 
   @Test void testSplitFilter() {
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java 
b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index 30aa2ff77f..7ad9a4d733 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -752,6 +752,76 @@ private void 
checkSimplify(UnaryOperator<RelBuilder.Config> transform,
     assertThat(f.apply(createBuilder()), hasTree(expected));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7657";>[CALCITE-7657]
+   * Apply the absorption law to simplify boolean expressions</a>. */
+  @Test void testFilterAndAbsorptionLaw() {
+    // Equivalent SQL:
+    //   SELECT *
+    //   FROM emp
+    //   WHERE deptno = 10 AND (deptno = 10 OR sal > 100)
+    // Should be simplified to:
+    //   SELECT *
+    //   FROM emp
+    //   WHERE deptno = 10
+    final Function<RelBuilder, RelNode> f = b ->
+        b.scan("EMP")
+            .filter(
+                b.and(
+                    b.equals(b.field("DEPTNO"), b.literal(10)),
+                    b.or(
+                        b.equals(b.field("DEPTNO"), b.literal(10)),
+                        b.greaterThan(b.field("SAL"), b.literal(100)))))
+            .build();
+
+    final String expected = "LogicalFilter(condition=[=($7, 10)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
+  @Test void testFilterOrAbsorptionLaw() {
+    // Equivalent SQL:
+    //   SELECT *
+    //   FROM emp
+    //   WHERE deptno = 10 OR (deptno = 10 AND sal > 100)
+    // Should be simplified to:
+    //   SELECT *
+    //   FROM emp
+    //   WHERE deptno = 10
+    final Function<RelBuilder, RelNode> f = b ->
+        b.scan("EMP")
+            .filter(
+                b.or(
+                    b.equals(b.field("DEPTNO"), b.literal(10)),
+                    b.and(
+                        b.equals(b.field("DEPTNO"), b.literal(10)),
+                        b.greaterThan(b.field("SAL"), b.literal(100)))))
+            .build();
+
+    final String expected = "LogicalFilter(condition=[=($7, 10)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
+  @Test void testFilterAbsorptionLawWithNonDeterministic() {
+    final Function<RelBuilder, RelNode> f = b -> {
+      final RexNode rand =
+          b.greaterThan(
+              b.call(SqlStdOperatorTable.RAND), b.literal(0.5));
+      return b.scan("EMP")
+          .filter(
+              b.and(rand,
+                  b.or(rand,
+                      b.greaterThan(b.field("SAL"), b.literal(100)))))
+          .build();
+    };
+
+    final String expected = "LogicalFilter(condition=[AND(>(RAND(), 0.5E0),"
+        + " OR(>(RAND(), 0.5E0), >($5, 100)))])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(f.apply(createBuilder()), hasTree(expected));
+  }
+
   @Test void testBadFieldName() {
     final RelBuilder builder = RelBuilder.create(config().build());
     try {
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index c8719eefe0..847cdb98d9 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -4327,7 +4327,7 @@ select * from "scott".emp where (empno, deptno) not in 
((1, 2), (3, null));
 
 !ok
 !if (use_old_decorr) {
-EnumerableCalc(expr#0..14=[{inputs}], expr#15=[0], expr#16=[=($t8, $t15)], 
expr#17=[IS NULL($t7)], expr#18=[IS NOT NULL($t13)], expr#19=[AND($t14, $t18)], 
expr#20=[<($t9, $t8)], expr#21=[OR($t17, $t19, $t18, $t20)], expr#22=[IS NOT 
TRUE($t21)], expr#23=[OR($t16, $t22)], proj#0..7=[{exprs}], $condition=[$t23])
+EnumerableCalc(expr#0..13=[{inputs}], expr#14=[0], expr#15=[=($t8, $t14)], 
expr#16=[IS NULL($t13)], expr#17=[>=($t9, $t8)], expr#18=[IS NOT NULL($t7)], 
expr#19=[AND($t16, $t17, $t18)], expr#20=[OR($t15, $t19)], proj#0..7=[{exprs}], 
$condition=[$t20])
   EnumerableMergeJoin(condition=[AND(=($10, $11), OR(IS NULL($12), 
=(CAST($7):INTEGER, $12)))], joinType=[left])
     EnumerableSort(sort0=[$10], dir0=[ASC])
       EnumerableCalc(expr#0..9=[{inputs}], expr#10=[CAST($t0):INTEGER NOT 
NULL], proj#0..10=[{exprs}])
@@ -4336,7 +4336,7 @@ EnumerableCalc(expr#0..14=[{inputs}], expr#15=[0], 
expr#16=[=($t8, $t15)], expr#
           EnumerableAggregate(group=[{}], c=[COUNT()], ck=[COUNT() FILTER $0])
             EnumerableValues(tuples=[[{ true }, { true }]])
     EnumerableSort(sort0=[$0], dir0=[ASC])
-      EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], expr#3=[IS NOT 
NULL($t1)], proj#0..3=[{exprs}])
+      EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], proj#0..2=[{exprs}])
         EnumerableValues(tuples=[[{ 3, null }, { 1, 2 }]])
 !plan
 !}
@@ -4351,14 +4351,14 @@ select * from "scott".emp where (mgr, deptno) not in 
((1, 2), (3, null), (cast(n
 
 !ok
 !if (use_old_decorr) {
-EnumerableCalc(expr#0..13=[{inputs}], expr#14=[0], expr#15=[=($t8, $t14)], 
expr#16=[IS NULL($t3)], expr#17=[IS NULL($t7)], expr#18=[IS NOT NULL($t12)], 
expr#19=[AND($t13, $t18)], expr#20=[<($t9, $t8)], expr#21=[OR($t16, $t17, $t19, 
$t18, $t20)], expr#22=[IS NOT TRUE($t21)], expr#23=[OR($t15, $t22)], 
proj#0..7=[{exprs}], $condition=[$t23])
+EnumerableCalc(expr#0..12=[{inputs}], expr#13=[0], expr#14=[=($t8, $t13)], 
expr#15=[IS NULL($t12)], expr#16=[>=($t9, $t8)], expr#17=[IS NOT NULL($t3)], 
expr#18=[IS NOT NULL($t7)], expr#19=[AND($t15, $t16, $t17, $t18)], 
expr#20=[OR($t14, $t19)], proj#0..7=[{exprs}], $condition=[$t20])
   EnumerableNestedLoopJoin(condition=[AND(OR(IS NULL($10), =(CAST($3):INTEGER, 
$10)), OR(IS NULL($11), =(CAST($7):INTEGER, $11)))], joinType=[left])
     EnumerableNestedLoopJoin(condition=[true], joinType=[inner])
       EnumerableTableScan(table=[[scott, EMP]])
       EnumerableAggregate(group=[{}], c=[COUNT()], ck=[COUNT() FILTER $0])
         EnumerableCalc(expr#0..1=[{inputs}], expr#2=[IS NOT NULL($t0)], 
expr#3=[IS NOT NULL($t1)], expr#4=[OR($t2, $t3)], $f2=[$t4])
           EnumerableValues(tuples=[[{ 3, null }, { null, null }, { 1, 2 }]])
-    EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], expr#3=[IS NOT 
NULL($t0)], expr#4=[IS NOT NULL($t1)], expr#5=[AND($t3, $t4)], expr#6=[OR($t3, 
$t4)], proj#0..2=[{exprs}], $f20=[$t5], $condition=[$t6])
+    EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], expr#3=[IS NOT 
NULL($t0)], expr#4=[IS NOT NULL($t1)], expr#5=[OR($t3, $t4)], 
proj#0..2=[{exprs}], $condition=[$t5])
       EnumerableValues(tuples=[[{ 3, null }, { null, null }, { 1, 2 }]])
 !plan
 !}
@@ -4393,7 +4393,7 @@ select * from "scott".emp where (empno, deptno) not in 
((7369, 20), (7499, 30));
 
 !ok
 !if (use_old_decorr) {
-EnumerableCalc(expr#0..15=[{inputs}], expr#16=[0], expr#17=[=($t8, $t16)], 
expr#18=[IS NULL($t7)], expr#19=[IS NOT NULL($t14)], expr#20=[AND($t15, $t19)], 
expr#21=[<($t9, $t8)], expr#22=[OR($t18, $t20, $t19, $t21)], expr#23=[IS NOT 
TRUE($t22)], expr#24=[OR($t17, $t23)], proj#0..7=[{exprs}], $condition=[$t24])
+EnumerableCalc(expr#0..14=[{inputs}], expr#15=[0], expr#16=[=($t8, $t15)], 
expr#17=[IS NULL($t14)], expr#18=[>=($t9, $t8)], expr#19=[IS NOT NULL($t7)], 
expr#20=[AND($t17, $t18, $t19)], expr#21=[OR($t16, $t20)], proj#0..7=[{exprs}], 
$condition=[$t21])
   EnumerableMergeJoin(condition=[AND(=($10, $12), =($11, $13))], 
joinType=[left])
     EnumerableSort(sort0=[$10], sort1=[$11], dir0=[ASC], dir1=[ASC])
       EnumerableCalc(expr#0..9=[{inputs}], expr#10=[CAST($t0):INTEGER NOT 
NULL], expr#11=[CAST($t7):INTEGER], proj#0..11=[{exprs}])
@@ -4403,7 +4403,7 @@ EnumerableCalc(expr#0..15=[{inputs}], expr#16=[0], 
expr#17=[=($t8, $t16)], expr#
             EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], $f2=[$t2])
               EnumerableValues(tuples=[[{ 7369, 20 }, { 7499, 30 }]])
     EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], dir1=[ASC])
-      EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], proj#0..2=[{exprs}], 
$f20=[$t2])
+      EnumerableCalc(expr#0..1=[{inputs}], expr#2=[true], proj#0..2=[{exprs}])
         EnumerableValues(tuples=[[{ 7369, 20 }, { 7499, 30 }]])
 !plan
 !}

Reply via email to