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

mihaibudiu 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 6ec931523d [CALCITE-7661] RelDecorrelator loses shared correlation 
constraint across inner join inputs
6ec931523d is described below

commit 6ec931523d39fb4f990518716f553aada59aba0f
Author: Darpan Lunagariya <[email protected]>
AuthorDate: Wed Jul 22 12:19:38 2026 +0530

    [CALCITE-7661] RelDecorrelator loses shared correlation constraint across 
inner join inputs
---
 .../apache/calcite/sql2rel/RelDecorrelator.java    | 18 +++--
 .../calcite/sql2rel/RelDecorrelatorTest.java       | 77 ++++++++++++++++++++++
 core/src/test/resources/sql/sub-query.iq           | 30 +++++++++
 3 files changed, 119 insertions(+), 6 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 b4999b455b..4e4104ad48 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -2074,12 +2074,18 @@ private static boolean isWidening(RelDataType type, 
RelDataType type1) {
       joinConditions.add(originalCond);
     }
 
-    if (generatesNullsOnLeft || generatesNullsOnRight) {
-      List<RexNode> conds =
-          buildCorDefJoinConditions(leftCorDefOutputs, rightCorDefOutputs,
-              newLeftFrame.r, newRightFrame.r, relBuilder);
-      joinConditions.addAll(conds);
-    }
+    // Decorrelation propagates references to outer columns as columns in the
+    // rewritten inputs. If both join inputs propagate the same reference, add
+    // a condition to ensure that they still represent the same outer value.
+    // This applies to every join type, regardless of whether it generates 
nulls;
+    // if the inputs have no references in common, no condition is added.
+    joinConditions.addAll(
+        buildCorDefJoinConditions(
+            newLeftFrame.corDefOutputs,
+            newRightFrame.corDefOutputs,
+            newLeftFrame.r,
+            newRightFrame.r,
+            relBuilder));
 
     RexNode finalCondition = joinConditions.isEmpty()
         ? relBuilder.literal(true)
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 240f9a36ae..51c1e89d2d 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
@@ -1737,6 +1737,83 @@ public static Frameworks.ConfigBuilder config() {
     assertThat(after, hasTree(planAfter));
   }
 
+  /**
+   * Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7661";>[CALCITE-7661]
+   * RelDecorrelator loses shared correlation constraint across inner join 
inputs</a>.
+   */
+  @Test void testDecorrelateInnerJoinWithSharedCorrelation() {
+    final FrameworkConfig frameworkConfig = config().build();
+    final RelBuilder builder = RelBuilder.create(frameworkConfig);
+    final RelOptCluster cluster = builder.getCluster();
+    final Planner planner = Frameworks.getPlanner(frameworkConfig);
+    final String sql = ""
+        + "SELECT d.deptno FROM dept d WHERE EXISTS (\n"
+        + "  SELECT *\n"
+        + "  FROM (SELECT * FROM emp e WHERE e.deptno = d.deptno) l\n"
+        + "  JOIN (SELECT * FROM dept d2 WHERE d2.deptno = d.deptno) r\n"
+        + "  ON TRUE)";
+    final RelNode originalRel;
+    try {
+      final SqlNode parse = planner.parse(sql);
+      final SqlNode validate = planner.validate(parse);
+      originalRel = planner.rel(validate).rel;
+    } catch (Exception e) {
+      throw TestUtil.rethrow(e);
+    }
+
+    final HepProgram hepProgram = HepProgram.builder()
+        .addRuleCollection(
+            ImmutableList.of(
+                CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
+                CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
+                CoreRules.JOIN_SUB_QUERY_TO_CORRELATE))
+        .build();
+    final Program program =
+        Programs.of(hepProgram, true,
+            requireNonNull(cluster.getMetadataProvider()));
+    final RelNode before =
+        program.run(cluster.getPlanner(), originalRel, cluster.traitSet(),
+            Collections.emptyList(), Collections.emptyList());
+    final String planBefore = ""
+        + "LogicalProject(DEPTNO=[$0])\n"
+        + "  LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2])\n"
+        + "    LogicalCorrelate(correlation=[$cor0], joinType=[inner], 
requiredColumns=[{0}])\n"
+        + "      LogicalTableScan(table=[[scott, DEPT]])\n"
+        + "      LogicalAggregate(group=[{0}])\n"
+        + "        LogicalProject(i=[true])\n"
+        + "          LogicalJoin(condition=[true], joinType=[inner])\n"
+        + "            LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], "
+        + "HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7])\n"
+        + "              LogicalFilter(condition=[=($7, $cor0.DEPTNO)])\n"
+        + "                LogicalTableScan(table=[[scott, EMP]])\n"
+        + "            LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2])\n"
+        + "              LogicalFilter(condition=[=($0, $cor0.DEPTNO)])\n"
+        + "                LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(before, hasTree(planBefore));
+
+    // Without the shared-carrier condition, the inner join becomes
+    // LogicalJoin(condition=[true]) and can join an employee to a different
+    // department.
+    final RelNode after =
+        RelDecorrelator.decorrelateQuery(before, builder, 
RuleSets.ofList(Collections.emptyList()),
+            RuleSets.ofList(Collections.emptyList()));
+    final String planAfter = ""
+        + "LogicalProject(DEPTNO=[$0])\n"
+        + "  LogicalJoin(condition=[=($0, $3)], joinType=[inner])\n"
+        + "    LogicalTableScan(table=[[scott, DEPT]])\n"
+        + "    LogicalProject(DEPTNO3=[$0], $f1=[true])\n"
+        + "      LogicalAggregate(group=[{0}])\n"
+        + "        LogicalProject(DEPTNO3=[$12])\n"
+        + "          LogicalJoin(condition=[IS NOT DISTINCT FROM($8, $12)], 
joinType=[inner])\n"
+        + "            LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], "
+        + "HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], DEPTNO8=[$7])\n"
+        + "              LogicalFilter(condition=[IS NOT NULL($7)])\n"
+        + "                LogicalTableScan(table=[[scott, EMP]])\n"
+        + "            LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], 
DEPTNO3=[$0])\n"
+        + "              LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(after, hasTree(planAfter));
+  }
+
   /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-5390";>[CALCITE-5390]
    * RelDecorrelator throws NullPointerException</a>. */
   @Test void testCorrelationLexicalScoping() {
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index daa85b9653..c8719eefe0 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -9624,6 +9624,36 @@ ON t2.a = foo.a
 
 !ok
 
+# [CALCITE-7661] RelDecorrelator loses shared correlation constraint across 
inner join inputs
+# Both join inputs reference d.deptno; decorrelation must keep their 
correlation carriers equal.
+SELECT d.deptno
+FROM dept d
+WHERE EXISTS (
+  SELECT *
+  FROM (
+    SELECT *
+    FROM emp e
+    WHERE e.deptno = d.deptno
+  ) l
+  JOIN (
+    SELECT *
+    FROM dept d2
+    WHERE d2.deptno = d.deptno
+  ) r
+  ON TRUE
+)
+ORDER BY d.deptno;
++--------+
+| DEPTNO |
++--------+
+|     10 |
+|     20 |
+|     30 |
++--------+
+(3 rows)
+
+!ok
+
 # [CALCITE-7320] AggregateProjectMergeRule throws AssertionError when Project 
maps multiple grouping keys to the same field
 SELECT deptno,
        (SELECT SUM(cnt)

Reply via email to