diff --git a/core/src/main/java/org/apache/calcite/sql2rel/CorrelateProjectExtractor.java b/core/src/main/java/org/apache/calcite/sql2rel/CorrelateProjectExtractor.java index 127f4e487941..a0c4747d2e7d 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/CorrelateProjectExtractor.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/CorrelateProjectExtractor.java @@ -18,6 +18,7 @@ import org.apache.calcite.rel.RelHomogeneousShuttle; import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.Correlate; import org.apache.calcite.rel.core.CorrelationId; import org.apache.calcite.rel.core.Filter; import org.apache.calcite.rel.core.Project; @@ -208,6 +209,11 @@ private static Set findCorrelationDependentCalls(CorrelationId corrId, SimpleCorrelationCollector finder = new SimpleCorrelationCollector(corrId); plan.accept(new RelHomogeneousShuttle() { @Override public RelNode visit(RelNode other) { + // Don't descend into a nested correlate that reuses this id: the refs below belong to it. + if (other instanceof Correlate + && ((Correlate) other).getCorrelationId().equals(corrId)) { + return other; + } if (other instanceof Project || other instanceof Filter) { other.accept(finder); } 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 80ef7f588ade..2757a7a49b64 100644 --- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java +++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java @@ -2548,4 +2548,43 @@ private RelNode decorrelateSql(String sql) { + " LogicalTableScan(table=[[bookstore, authors]])\n"; assertThat(after, hasTree(planAfter)); } + + /** Test case for + * [CALCITE-7753] + * CorrelateProjectExtractor corrupts plans with nested Correlates that reuse + * the same correlation id. */ + @Test void testNestedCorrelatesSharingCorrelationId() { + final RelBuilder builder = RelBuilder.create(config().build()) + .transform(c -> c.withSimplify(false)); + final Holder<@Nullable RexCorrelVariable> v = Holder.empty(); + builder.scan("EMP").variable(v::set); + final RelNode emp = builder.build(); + + final RelNode inner = builder + .scan("EMP") + .scan("EMP") + .filter(builder.equals(builder.field(v.get(), "DEPTNO"), builder.field("DEPTNO"))) + .correlate(JoinRelType.INNER, v.get().id, builder.field(2, 0, "DEPTNO")) + .filter(builder.isNull(builder.field(v.get(), "COMM"))) + .build(); + + final RelNode before = builder + .push(emp) + .push(inner) + .correlate(JoinRelType.LEFT, v.get().id, builder.field(2, 0, "COMM")) + .build(); + final String planBefore = "" + + "LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{6}])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalFilter(condition=[IS NULL($cor0.COMM)])\n" + + " LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{7}])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalFilter(condition=[=($cor0.DEPTNO, $7)])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(before, hasTree(planBefore)); + + RelDecorrelator.decorrelateQuery(before, builder, + RuleSets.ofList(Collections.emptyList()), + RuleSets.ofList(Collections.emptyList())); + } }