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)