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 779e37de8f [CALCITE-7915] RelDecorrelator does not recognize IS NOT 
DISTINCT FROM as correlation equivalent
779e37de8f is described below

commit 779e37de8fa9a360eb4f0bd46f7206ead01dfced
Author: Yu Xu <[email protected]>
AuthorDate: Thu Nov 5 20:49:29 2026 +0800

    [CALCITE-7915] RelDecorrelator does not recognize IS NOT DISTINCT FROM as 
correlation equivalent
---
 .../apache/calcite/sql2rel/RelDecorrelator.java    | 44 ++++++++++++++++++----
 .../calcite/sql2rel/RelDecorrelatorTest.java       | 37 ++++++++++++++++++
 2 files changed, 73 insertions(+), 8 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java 
b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
index 0701e592ff..1f5272f29d 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -1757,21 +1757,29 @@ private Frame createFrameWithValueGenerator(RelNode 
rel, Frame frame,
    * ensuring that we only map the correlation variable to a local field or
    * expression from the current relational expression (e.g., a {@link 
RexInputRef}),
    * rather than to another correlation variable.
+   *
+   * <p>{@code IS NOT DISTINCT FROM} establishes the same equivalence as
+   * {@code =} (it compares values null-safely), so it is also recognized.
    */
   private static void findCorrelationEquivalent(CorRef correlation, RexNode e)
       throws Util.FoundOne {
     switch (e.getKind()) {
     case EQUALS:
+    case IS_NOT_DISTINCT_FROM:
       final RexCall call = (RexCall) e;
       final List<RexNode> operands = call.getOperands();
-      if (!RexUtil.containsFieldAccess(operands.get(1))
-          && references(operands.get(0), correlation)) {
-        throw new Util.FoundOne(operands.get(1));
-      }
-      if (!RexUtil.containsFieldAccess(operands.get(0))
-          && references(operands.get(1), correlation)) {
-        throw new Util.FoundOne(operands.get(0));
-      }
+      // For IS NOT DISTINCT FROM, binding the correlation to a local operand
+      // is only sound if that operand is NOT NULL. Decorrelation leaves the
+      // original filter in place on the inner side of the join, with the
+      // correlation substituted by its binding, so the filter condition
+      // becomes the residual condition {@code IS NOT DISTINCT FROM(x, x)},
+      // which is always true (hence harmless). The null-safe comparison of
+      // the original condition is instead enforced by the join condition.
+      // If x were nullable, inner rows with x = NULL, which the original
+      // condition accepts when the correlated column is null, would be
+      // rejected by the null-rejecting join condition, changing the result.
+      findEqualityEquivalent(correlation, operands.get(0), operands.get(1),
+          e.getKind() == SqlKind.IS_NOT_DISTINCT_FROM);
       break;
     case AND:
       for (RexNode operand : ((RexCall) e).getOperands()) {
@@ -1783,6 +1791,26 @@ && references(operands.get(1), correlation)) {
     }
   }
 
+  /** If one of {@code operand0} and {@code operand1} references the given
+   * correlation, and the other does not contain any {@link RexFieldAccess},
+   * throws the other operand as a {@link 
org.apache.calcite.util.Util.FoundOne}.
+   *
+   * <p>If {@code requireNotNullOperand} is true, the non-correlation operand
+   * must additionally be NOT NULL to be accepted. */
+  private static void findEqualityEquivalent(CorRef correlation, RexNode 
operand0,
+      RexNode operand1, boolean requireNotNullOperand) throws Util.FoundOne {
+    if (!RexUtil.containsFieldAccess(operand1)
+        && references(operand0, correlation)
+        && !(requireNotNullOperand && operand1.getType().isNullable())) {
+      throw new Util.FoundOne(operand1);
+    }
+    if (!RexUtil.containsFieldAccess(operand0)
+        && references(operand1, correlation)
+        && !(requireNotNullOperand && operand0.getType().isNullable())) {
+      throw new Util.FoundOne(operand0);
+    }
+  }
+
   private static boolean references(RexNode e, CorRef correlation) {
     switch (e.getKind()) {
     case CAST:
diff --git 
a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java 
b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
index d8f1fb54b2..e59b4c163c 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
@@ -657,6 +657,43 @@ public static Frameworks.ConfigBuilder config() {
     assertThat(after, hasTree(planAfter));
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7915";>[CALCITE-7915]
+   * RelDecorrelator does not recognize IS NOT DISTINCT FROM as correlation 
equivalent</a>. */
+  @Test void testCorrelationEquivalentIsNotDistinctFrom() {
+    final RelBuilder builder = RelBuilder.create(config().build());
+    // Simplification is disabled to keep the IS NOT DISTINCT FROM condition
+    // intact (otherwise it would be rewritten in terms of = and IS NULL).
+    final RelBuilder noSimplify = builder.transform(c -> 
c.withSimplify(false));
+    final Holder<@Nullable RexCorrelVariable> v = Holder.empty();
+    RelNode before = noSimplify.scan("EMP")
+        .variable(v::set)
+        .scan("DEPT")
+        .filter(
+            noSimplify.call(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM,
+                noSimplify.field(0),
+                noSimplify.field(v.get(), "DEPTNO")))
+        .correlate(JoinRelType.LEFT, v.get().id, noSimplify.field(2, 0, 
"DEPTNO"))
+        .build();
+
+    final String planBefore = ""
+        + "LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{7}])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n"
+        + "  LogicalFilter(condition=[IS NOT DISTINCT FROM($0, 
$cor0.DEPTNO)])\n"
+        + "    LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(before, hasTree(planBefore));
+
+    RelNode after = RelDecorrelator.decorrelateQuery(before, builder);
+    // The correlation variable is bound to the local DEPTNO column (found
+    // through the IS NOT DISTINCT FROM condition), so no value generator is
+    // needed; the null-safe comparison is enforced by the join condition.
+    final String planAfter = ""
+        + "LogicalJoin(condition=[=($7, $8)], joinType=[left])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n"
+        + "  LogicalFilter(condition=[IS NOT DISTINCT FROM($0, $0)])\n"
+        + "    LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(after, hasTree(planAfter));
+  }
+
   @Test void testCorrelationInSetOp0() {
     final FrameworkConfig frameworkConfig = config().build();
     final RelBuilder builder = RelBuilder.create(frameworkConfig);

Reply via email to