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 e6265a318e [CALCITE-7646] CorrelateProjectExtractor does not handle 
nested field accesses cor0.field0.field1
e6265a318e is described below

commit e6265a318ee943e5b4a79cb617db7b47422d6123
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Jul 6 14:45:57 2026 -0700

    [CALCITE-7646] CorrelateProjectExtractor does not handle nested field 
accesses cor0.field0.field1
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../calcite/sql2rel/CorrelateProjectExtractor.java | 77 ++++++++++++++---
 .../sql2rel/CorrelateProjectExtractorTest.java     | 72 ++++++++++++++++
 .../calcite/sql2rel/RelDecorrelatorTest.java       | 99 ++++++++++++++++++----
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 20 ++---
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 40 ++++-----
 core/src/test/resources/sql/lateral.iq             | 94 ++++++++++++++++++++
 6 files changed, 341 insertions(+), 61 deletions(-)

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 50d255c331..127f4e4879 100644
--- 
a/core/src/main/java/org/apache/calcite/sql2rel/CorrelateProjectExtractor.java
+++ 
b/core/src/main/java/org/apache/calcite/sql2rel/CorrelateProjectExtractor.java
@@ -88,6 +88,20 @@ public CorrelateProjectExtractor(RelBuilderFactory factory) {
     this.builderFactory = factory;
   }
 
+  /** Returns whether {@code node} is a direct field access on the correlation
+   * variable with the specified id, such as {@code $cor0.DEPTNO}. A nested
+   * access such as {@code $cor0.REC.DEPTNO} is not direct. */
+  private static boolean isDirectFieldAccess(RexNode node, CorrelationId id) {
+    if (node instanceof RexFieldAccess) {
+      RexFieldAccess access = (RexFieldAccess) node;
+      if (access.getReferenceExpr() instanceof RexCorrelVariable) {
+        RexCorrelVariable correlVar = (RexCorrelVariable) 
access.getReferenceExpr();
+        return correlVar.id.equals(id);
+      }
+    }
+    return false;
+  }
+
   @Override public RelNode visit(LogicalCorrelate correlate) {
     RelNode left = correlate.getLeft().accept(this);
     RelNode right = correlate.getRight().accept(this);
@@ -95,8 +109,12 @@ public CorrelateProjectExtractor(RelBuilderFactory factory) 
{
     // Find the correlated expressions from the right side that can be moved 
to the left
     Set<RexNode> callsWithCorrelationInRight =
         findCorrelationDependentCalls(correlate.getCorrelationId(), right);
+    // Only direct field accesses on the correlation variable, such as
+    // $cor0.DEPTNO, are left in place. A nested field access, such as
+    // $cor0.REC.DEPTNO, is extracted
     boolean isTrivialCorrelation =
-        callsWithCorrelationInRight.stream().allMatch(exp -> exp instanceof 
RexFieldAccess);
+        callsWithCorrelationInRight.stream()
+            .allMatch(exp -> isDirectFieldAccess(exp, 
correlate.getCorrelationId()));
     // Early exit condition
     if (isTrivialCorrelation) {
       if (correlate.getLeft().equals(left) && 
correlate.getRight().equals(right)) {
@@ -116,27 +134,42 @@ public CorrelateProjectExtractor(RelBuilderFactory 
factory) {
     // Transform the correlated expression from the right side to an 
expression over the left side
     builder.push(left);
 
+    ImmutableBitSet.Builder requiredColumns = ImmutableBitSet.builder();
     List<RexNode> callsWithCorrelationOverLeft = new ArrayList<>();
     for (RexNode callInRight : callsWithCorrelationInRight) {
-      
callsWithCorrelationOverLeft.add(replaceCorrelationsWithInputRef(callInRight, 
builder));
+      if (isDirectFieldAccess(callInRight, correlate.getCorrelationId())) {
+        // Direct field accesses stay in the right side and keep reading their
+        // original left column; that column must remain a required column.
+        requiredColumns.set(((RexFieldAccess) 
callInRight).getField().getIndex());
+      } else {
+        
callsWithCorrelationOverLeft.add(replaceCorrelationsWithInputRef(callInRight, 
builder));
+      }
     }
     builder.projectPlus(callsWithCorrelationOverLeft);
 
     // Construct the mapping to transform the expressions in the right side 
based on the new
     // projection in the left side.
     Map<RexNode, RexNode> transformMapping = new HashMap<>();
+    int newFieldIndex = oldLeft;
     for (RexNode callInRight : callsWithCorrelationInRight) {
-      RexBuilder xb = builder.getRexBuilder();
-      RexNode v = xb.makeCorrel(builder.peek().getRowType(), 
correlate.getCorrelationId());
-      RexNode flatCorrelationInRight = xb.makeFieldAccess(v, oldLeft + 
transformMapping.size());
-      transformMapping.put(callInRight, flatCorrelationInRight);
+      if (!isDirectFieldAccess(callInRight, correlate.getCorrelationId())) {
+        RexBuilder xb = builder.getRexBuilder();
+        RexNode v = xb.makeCorrel(builder.peek().getRowType(), 
correlate.getCorrelationId());
+        RexNode flatCorrelationInRight = xb.makeFieldAccess(v, newFieldIndex);
+        transformMapping.put(callInRight, flatCorrelationInRight);
+        newFieldIndex++;
+      }
     }
 
-    // Select the required fields/columns from the left side of the 
correlation. Based on the code
-    // above all these fields should be at the end of the left relational 
expression.
+    // Select the required fields/columns from the left side of the 
correlation: the columns
+    // read by the direct field accesses plus the newly projected columns, 
which are at the
+    // end of the left relational expression.
     List<RexNode> requiredFields =
         builder.fields(
-            ImmutableBitSet.range(oldLeft, oldLeft + 
callsWithCorrelationOverLeft.size()).asList());
+            requiredColumns
+                .set(oldLeft, oldLeft + callsWithCorrelationOverLeft.size())
+                .build()
+                .asList());
 
     final int newLeft = builder.fields().size();
     // Transform the expressions in the right side using the mapping 
constructed earlier.
@@ -264,8 +297,13 @@ private static boolean 
isSimpleCorrelatedExpression(RexNode node, CorrelationId
    * +(10, $cor0.DEPTNO) -> TRUE
    * /(100,+(10, $cor0.DEPTNO)) -> TRUE
    * CAST(+(10, $cor0.DEPTNO)):INTEGER NOT NULL -> TRUE
+   * CASE(IS NOT NULL($cor0.PATH), $cor0.PATH, ARRAY(null:INTEGER)) -> TRUE
    * +($0, $cor0.DEPTNO) -> FALSE
    * }</pre>
+   *
+   * <p>A subexpression built only from literals and dynamic parameters, such
+   * as {@code ARRAY(null:INTEGER)} above, is neutral: it neither qualifies nor
+   * disqualifies the enclosing call.
    */
   private static class SimpleCorrelationDetector
       extends RexVisitorImpl<@Nullable Boolean> {
@@ -284,7 +322,8 @@ private SimpleCorrelationDetector(CorrelationId corrId) {
       return Boolean.FALSE;
     }
 
-    @Override public Boolean visitCall(RexCall call) {
+    @Override public @Nullable Boolean visitCall(RexCall call) {
+      // Constant operands must not disqualify the call
       Boolean hasSimpleCorrelation = null;
       for (RexNode op : call.operands) {
         Boolean b = op.accept(this);
@@ -292,7 +331,8 @@ private SimpleCorrelationDetector(CorrelationId corrId) {
           hasSimpleCorrelation = hasSimpleCorrelation == null ? b : 
hasSimpleCorrelation && b;
         }
       }
-      return hasSimpleCorrelation == null ? Boolean.FALSE : 
hasSimpleCorrelation;
+      // If unsure return null; caller will decide
+      return hasSimpleCorrelation;
     }
 
     @Override public @Nullable Boolean visitFieldAccess(RexFieldAccess 
fieldAccess) {
@@ -332,8 +372,10 @@ private static RexNode 
replaceCorrelationsWithInputRef(RexNode exp, RelBuilder b
   }
 
   /**
-   * A visitor traversing row expressions and replacing calls with other 
expressions according
-   * to the specified mapping.
+   * A visitor traversing row expressions and replacing calls and field
+   * accesses with other expressions according to the specified mapping.
+   * The mapping is consulted before recursing so that the outermost
+   * matching expression wins.
    */
   private static final class CallReplacer extends RexShuttle {
     private final Map<RexNode, RexNode> mapping;
@@ -350,5 +392,14 @@ private static final class CallReplacer extends RexShuttle 
{
         return super.visitCall(oldCall);
       }
     }
+
+    @Override public RexNode visitFieldAccess(RexFieldAccess fieldAccess) {
+      RexNode replacement = mapping.get(fieldAccess);
+      if (replacement != null) {
+        return replacement;
+      } else {
+        return super.visitFieldAccess(fieldAccess);
+      }
+    }
   }
 }
diff --git 
a/core/src/test/java/org/apache/calcite/sql2rel/CorrelateProjectExtractorTest.java
 
b/core/src/test/java/org/apache/calcite/sql2rel/CorrelateProjectExtractorTest.java
index 72276d42c3..da0a17296b 100644
--- 
a/core/src/test/java/org/apache/calcite/sql2rel/CorrelateProjectExtractorTest.java
+++ 
b/core/src/test/java/org/apache/calcite/sql2rel/CorrelateProjectExtractorTest.java
@@ -84,6 +84,78 @@ public static Frameworks.ConfigBuilder config() {
     assertThat(after, hasTree(planAfter));
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7646";>[CALCITE-7646]
+   * CorrelateProjectExtractor does not handle nested field accesses 
cor0.field0.field1</a>. */
+  @Test void testNestedCorrelationFieldAccessInFilter() {
+    final RelBuilder builder = RelBuilder.create(config().build());
+    final Holder<@Nullable RexCorrelVariable> v = Holder.empty();
+    RelNode before = builder.scan("EMP")
+        .project(
+            builder.alias(
+                builder.call(SqlStdOperatorTable.ROW,
+                    builder.field("EMPNO"), builder.field("DEPTNO")), "R"))
+        .variable(v::set)
+        .scan("DEPT")
+        .filter(
+            builder.equals(builder.field(0),
+                builder.getRexBuilder().makeFieldAccess(builder.field(v.get(), 
"R"), 1)))
+        .correlate(JoinRelType.LEFT, v.get().id, builder.field(2, 0, "R"))
+        .build();
+
+    final String planBefore = ""
+        + "LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{0}])\n"
+        + "  LogicalProject(R=[ROW($0, $7)])\n"
+        + "    LogicalTableScan(table=[[scott, EMP]])\n"
+        + "  LogicalFilter(condition=[=($0, $cor0.R.EXPR$1)])\n"
+        + "    LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(before, hasTree(planBefore));
+
+    RelNode after = before.accept(new 
CorrelateProjectExtractor(RelFactories.LOGICAL_BUILDER));
+    final String planAfter = ""
+        + "LogicalProject(R=[$0], DEPTNO=[$2], DNAME=[$3], LOC=[$4])\n"
+        + "  LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{1}])\n"
+        + "    LogicalProject(R=[ROW($0, $7)], $f1=[ROW($0, $7).EXPR$1])\n"
+        + "      LogicalTableScan(table=[[scott, EMP]])\n"
+        + "    LogicalFilter(condition=[=($0, $cor0.$f1)])\n"
+        + "      LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(after, hasTree(planAfter));
+  }
+
+  /** Tests that a constant call operand, such as {@code POWER(2, 3)}, does
+   * not prevent extracting the enclosing correlated call. */
+  @Test void testCorrelationCallWithConstantCallOperandInFilter() {
+    final RelBuilder builder = RelBuilder.create(config().build());
+    final Holder<@Nullable RexCorrelVariable> v = Holder.empty();
+    RelNode before = builder.scan("EMP")
+        .variable(v::set)
+        .scan("DEPT")
+        .filter(
+            builder.equals(builder.field(0),
+                builder.call(SqlStdOperatorTable.PLUS,
+                    builder.call(SqlStdOperatorTable.POWER,
+                        builder.literal(2), builder.literal(3)),
+                    builder.field(v.get(), "DEPTNO"))))
+        .correlate(JoinRelType.LEFT, v.get().id, builder.field(2, 0, "DEPTNO"))
+        .build();
+
+    final String planBefore = ""
+        + "LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{7}])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n"
+        + "  LogicalFilter(condition=[=($0, +(POWER(2, 3), $cor0.DEPTNO))])\n"
+        + "    LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(before, hasTree(planBefore));
+
+    RelNode after = before.accept(new 
CorrelateProjectExtractor(RelFactories.LOGICAL_BUILDER));
+    final String planAfter = ""
+        + "LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], DEPTNO0=[$9], DNAME=[$10], 
LOC=[$11])\n"
+        + "  LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{8}])\n"
+        + "    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], $f8=[+(POWER(2, 3), $7)])\n"
+        + "      LogicalTableScan(table=[[scott, EMP]])\n"
+        + "    LogicalFilter(condition=[=($0, $cor0.$f8)])\n"
+        + "      LogicalTableScan(table=[[scott, DEPT]])\n";
+    assertThat(after, hasTree(planAfter));
+  }
+
   @Test void testDoubleCorrelationCallOverVariableInFilters() {
     final RelBuilder builder = RelBuilder.create(config().build());
     final Holder<@Nullable RexCorrelVariable> v = Holder.empty();
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..ac218b220a 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
@@ -290,9 +290,9 @@ public static Frameworks.ConfigBuilder config() {
         + "    LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])\n"
         + "      LogicalValues(tuples=[[{ 7369 }, { 7499 }]])\n"
         + "      LogicalAggregate(group=[{0}], agg#0=[SINGLE_VALUE($1)])\n"
-        + "        LogicalProject(EMPNO1=[$12], EXPR$0=[||(||($1, ' from dept 
'), $13)])\n"
-        + "          LogicalJoin(condition=[AND(=($7, $10), =($9, $11))], 
joinType=[left])\n"
-        + "            LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], DEPTNO0=[$7], 
EMPNO0=[CAST($0):INTEGER NOT NULL])\n"
+        + "        LogicalProject(EMPNO1=[$11], EXPR$0=[||(||($1, ' from dept 
'), $12)])\n"
+        + "          LogicalJoin(condition=[AND(=($7, $9), =($8, $10))], 
joinType=[left])\n"
+        + "            LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], 
EMPNO0=[CAST($0):INTEGER NOT NULL])\n"
         + "              LogicalTableScan(table=[[scott, EMP]])\n"
         + "            LogicalAggregate(group=[{0, 1, 2}], 
agg#0=[SINGLE_VALUE($3)])\n"
         + "              LogicalProject(DEPTNO0=[$3], EMPNO0=[$4], EMPNO=[$5], 
DNAME=[$1])\n"
@@ -556,29 +556,29 @@ public static Frameworks.ConfigBuilder config() {
     //                                  LogicalTableScan(table=[[scott, EMP]])
     final String planAfter = ""
         + "LogicalSort(sort0=[$0], dir0=[ASC])\n"
-        + "  LogicalProject(DNAME=[$1], C=[$7])\n"
-        + "    LogicalJoin(condition=[AND(=($0, $5), =($4, $6))], 
joinType=[left])\n"
-        + "      LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], 
DEPTNO0=[$0], $f4=[*($0, 100)])\n"
+        + "  LogicalProject(DNAME=[$1], C=[$6])\n"
+        + "    LogicalJoin(condition=[AND(=($0, $4), =($3, $5))], 
joinType=[left])\n"
+        + "      LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], $f3=[*($0, 
100)])\n"
         + "        LogicalTableScan(table=[[scott, DEPT]])\n"
-        + "      LogicalProject(DEPTNO8=[$0], $f4=[$1], EXPR$0=[CASE(IS NOT 
NULL($4), $4, 0)])\n"
+        + "      LogicalProject(DEPTNO8=[$0], $f3=[$1], EXPR$0=[CASE(IS NOT 
NULL($4), $4, 0)])\n"
         + "        LogicalJoin(condition=[AND(IS NOT DISTINCT FROM($0, $2), IS 
NOT DISTINCT FROM($1, $3))], joinType=[left])\n"
-        + "          LogicalProject(DEPTNO=[$0], $f4=[*($0, 100)])\n"
+        + "          LogicalProject(DEPTNO=[$0], $f3=[*($0, 100)])\n"
         + "            LogicalTableScan(table=[[scott, DEPT]])\n"
         + "          LogicalAggregate(group=[{0, 1}], EXPR$0=[COUNT()])\n"
-        + "            LogicalProject(DEPTNO8=[$7], $f4=[$9])\n"
+        + "            LogicalProject(DEPTNO8=[$7], $f3=[$9])\n"
         + "              LogicalFilter(condition=[IS NOT NULL($7)])\n"
-        + "                LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], i=[$11], $f4=[$9])\n"
+        + "                LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], i=[$11], $f3=[$9])\n"
         + "                  LogicalJoin(condition=[=($8, $10)], 
joinType=[inner])\n"
         + "                    LogicalProject(EMPNO=[$0], ENAME=[$1], 
JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], 
SAL0=[CAST($5):DECIMAL(12, 2)])\n"
         + "                      LogicalTableScan(table=[[scott, EMP]])\n"
-        + "                    LogicalProject($f4=[$0], SAL0=[$1], 
$f2=[true])\n"
+        + "                    LogicalProject($f3=[$0], SAL0=[$1], 
$f2=[true])\n"
         + "                      LogicalAggregate(group=[{0, 1}])\n"
-        + "                        LogicalProject($f4=[$1], SAL0=[$2])\n"
+        + "                        LogicalProject($f3=[$1], SAL0=[$2])\n"
         + "                          LogicalJoin(condition=[AND(>($2, 
CAST($0):DECIMAL(12, 2) NOT NULL), <($1, $0))], joinType=[inner])\n"
         + "                            LogicalValues(tuples=[[{ 1000 }, { 2000 
}, { 3000 }]])\n"
         + "                            LogicalJoin(condition=[true], 
joinType=[inner])\n"
         + "                              LogicalAggregate(group=[{0}])\n"
-        + "                                LogicalProject($f4=[*($0, 100)])\n"
+        + "                                LogicalProject($f3=[*($0, 100)])\n"
         + "                                  LogicalTableScan(table=[[scott, 
DEPT]])\n"
         + "                              LogicalAggregate(group=[{0}])\n"
         + "                                
LogicalProject(SAL0=[CAST($5):DECIMAL(12, 2)])\n"
@@ -1793,9 +1793,9 @@ public static Frameworks.ConfigBuilder config() {
         RelDecorrelator.decorrelateQuery(before, builder, 
RuleSets.ofList(Collections.emptyList()),
             RuleSets.ofList(Collections.emptyList()));
     final String planAfter = ""
-        + "LogicalProject(DEPTNO=[$0], I0=[$3], I1=[$8])\n"
-        + "  LogicalJoin(condition=[AND(=($0, $6), =($5, $7))], 
joinType=[left])\n"
-        + "    LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EXPR$0=[$5], 
DEPTNO0=[$0], $f5=[>(CAST($0):INTEGER NOT NULL, 0)])\n"
+        + "LogicalProject(DEPTNO=[$0], I0=[$3], I1=[$7])\n"
+        + "  LogicalJoin(condition=[AND(=($0, $5), =($4, $6))], 
joinType=[left])\n"
+        + "    LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EXPR$0=[$5], 
$f4=[>(CAST($0):INTEGER NOT NULL, 0)])\n"
         + "      LogicalJoin(condition=[=($3, $4)], joinType=[left])\n"
         + "        LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], 
DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
         + "          LogicalTableScan(table=[[scott, DEPT]])\n"
@@ -1807,12 +1807,12 @@ public static Frameworks.ConfigBuilder config() {
         + "                LogicalProject(DEPTNO0=[CAST($0):SMALLINT NOT 
NULL])\n"
         + "                  LogicalTableScan(table=[[scott, DEPT]])\n"
         + "    LogicalAggregate(group=[{0, 1}], EXPR$0=[MIN($2)])\n"
-        + "      LogicalProject(DEPTNO0=[$8], $f5=[$9], $f0=[0])\n"
+        + "      LogicalProject(DEPTNO0=[$8], $f4=[$9], $f0=[0])\n"
         + "        LogicalJoin(condition=[=($7, $8)], joinType=[inner])\n"
         + "          LogicalFilter(condition=[=($1, 'SMITH')])\n"
         + "            LogicalTableScan(table=[[scott, EMP]])\n"
         + "          LogicalFilter(condition=[$1])\n"
-        + "            LogicalProject(DEPTNO=[$0], $f5=[>(CAST($0):INTEGER NOT 
NULL, 0)])\n"
+        + "            LogicalProject(DEPTNO=[$0], $f4=[>(CAST($0):INTEGER NOT 
NULL, 0)])\n"
         + "              LogicalJoin(condition=[=($3, $4)], joinType=[left])\n"
         + "                LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], 
DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
         + "                  LogicalTableScan(table=[[scott, DEPT]])\n"
@@ -2351,4 +2351,67 @@ public static Frameworks.ConfigBuilder config() {
         + "                  LogicalTableScan(table=[[scott, EMP]])\n";
     assertThat(after, hasTree(planAfter));
   }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7646";>[CALCITE-7646]
+   * CorrelateProjectExtractor does not handle nested field accesses 
cor0.field0.field1</a>. */
+  @Test void testNestedCorrelatedFieldAccess() throws SqlParseException {
+    final String sql = "select a.\"aid\", t.lat\n"
+        + "from \"bookstore\".\"authors\" a,\n"
+        + "lateral (select b.\"aid\" as c,\n"
+        + "    (a.\"birthPlace\").\"coords\".\"latitude\" as lat\n"
+        + "  from \"bookstore\".\"authors\" b\n"
+        + "  where b.\"aid\" = a.\"aid\") as t";
+    SchemaPlus rootSchema = Frameworks.createRootSchema(true);
+    CalciteAssert.addSchema(rootSchema, CalciteAssert.SchemaSpec.BOOKSTORE);
+    CalciteConnectionConfig config = new CalciteConnectionConfigImpl(new 
Properties());
+    // The Frameworks planner cannot be used here because it flattens
+    // structured types, and RelStructuredTypeFlattener does not support
+    // correlations on structured columns.
+    SqlTestFactory factory = SqlTestFactory.INSTANCE
+        .withCatalogReader((typeFactory, caseSensitive) ->
+            new CalciteCatalogReader(
+                CalciteSchema.from(rootSchema),
+                ImmutableList.of("bookstore"),
+                typeFactory,
+                config));
+    SqlParser parser = factory.createParser(sql);
+    SqlNode parsed = parser.parseQuery();
+    final SqlToRelConverter sqlToRelConverter = 
factory.createSqlToRelConverter();
+    assert sqlToRelConverter.validator != null;
+    final SqlNode validated = sqlToRelConverter.validator.validate(parsed);
+    final RelNode before = sqlToRelConverter.convertQuery(validated, false, 
true).rel;
+
+    final String planBefore = ""
+        + "LogicalProject(aid=[$0], LAT=[$5])\n"
+        + "  LogicalCorrelate(correlation=[$cor1], joinType=[inner], 
requiredColumns=[{0, 2}])\n"
+        + "    LogicalTableScan(table=[[bookstore, authors]])\n"
+        + "    LogicalProject(C=[$0], 
LAT=[$cor1.birthPlace.coords.latitude])\n"
+        + "      LogicalFilter(condition=[=($0, $cor1.aid)])\n"
+        + "        LogicalTableScan(table=[[bookstore, authors]])\n";
+    assertThat(before, hasTree(planBefore));
+
+    final RelBuilder relBuilder =
+        RelFactories.LOGICAL_BUILDER.create(before.getCluster(), null);
+    // Decorrelate without any rules, just "purely" decorrelation algorithm on 
RelDecorrelator
+    final RelNode after =
+        RelDecorrelator.decorrelateQuery(before, relBuilder,
+            RuleSets.ofList(Collections.emptyList()),
+            RuleSets.ofList(Collections.emptyList()));
+
+    // The nested field access is extracted into the projection $f4 on the
+    // left side and no correlation variables remain.
+    final String planAfter = ""
+        + "LogicalProject(aid=[$0], LAT=[$6])\n"
+        + "  LogicalJoin(condition=[AND(=($0, $7), IS NOT DISTINCT FROM($4, 
$8))], joinType=[inner])\n"
+        + "    LogicalProject(aid=[$0], name=[$1], birthPlace=[$2], 
books=[$3], $f4=[$2.coords.latitude])\n"
+        + "      LogicalTableScan(table=[[bookstore, authors]])\n"
+        + "    LogicalProject(C=[$0], LAT=[$4], aid=[$0], $f4=[$4])\n"
+        + "      LogicalJoin(condition=[true], joinType=[inner])\n"
+        + "        LogicalTableScan(table=[[bookstore, authors]])\n"
+        + "        LogicalAggregate(group=[{0}])\n"
+        + "          LogicalProject($f4=[$2.coords.latitude])\n"
+        + "            LogicalTableScan(table=[[bookstore, authors]])\n";
+    assertThat(after, hasTree(planAfter));
+  }
 }
diff --git 
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml 
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index d7c03e93f5..1e37b2699b 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -3097,9 +3097,9 @@ LogicalProject(NAME=[$1], EXPR$1=[$2])
     </Resource>
     <Resource name="planAfter">
       <![CDATA[
-LogicalProject(NAME=[$1], EXPR$1=[$4])
-  LogicalJoin(condition=[AND(=($0, $5), =($3, $6))], joinType=[left])
-    LogicalProject(DEPTNO=[$0], NAME=[$1], DEPTNO0=[$0], 
NAME0=[CAST($1):VARCHAR(20) NOT NULL])
+LogicalProject(NAME=[$1], EXPR$1=[$3])
+  LogicalJoin(condition=[AND(=($0, $4), =($2, $5))], joinType=[left])
+    LogicalProject(DEPTNO=[$0], NAME=[$1], NAME0=[CAST($1):VARCHAR(20) NOT 
NULL])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
     LogicalProject(SAL=[$0], DEPTNO=[$2], ENAME=[$3])
       LogicalFilter(condition=[<=($4, 1)])
@@ -8815,19 +8815,19 @@ where n1.SAL IN (
     <Resource name="planBefore">
       <![CDATA[
 LogicalProject(SAL=[$5])
-  LogicalJoin(condition=[AND(=($5, $11), IS NOT DISTINCT FROM($9, $12))], 
joinType=[inner])
-    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], SLACKER=[$8], SAL0=[$5], $f9=[=($5, 4)])
+  LogicalJoin(condition=[AND(=($5, $10), IS NOT DISTINCT FROM($8, $11))], 
joinType=[inner])
+    LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], SLACKER=[$8], $f8=[=($5, 4)])
       LogicalFilter(condition=[AND(=($7, 20), >($5, 1000))])
         LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
     LogicalFilter(condition=[=($1, $0)])
       LogicalAggregate(group=[{0, 1, 2}])
-        LogicalProject(SAL=[$5], SAL0=[$8], $f9=[$9])
+        LogicalProject(SAL=[$5], SAL0=[$8], $f8=[$9])
           LogicalJoin(condition=[OR(=($8, $5), $9)], joinType=[inner])
             LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], SLACKER=[$8])
               LogicalFilter(condition=[AND(=($7, 20), >($5, 1000))])
                 LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
             LogicalAggregate(group=[{0, 1}])
-              LogicalProject(SAL=[$5], $f9=[=($5, 4)])
+              LogicalProject(SAL=[$5], $f8=[=($5, 4)])
                 LogicalFilter(condition=[AND(=($7, 20), >($5, 1000))])
                   LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
 ]]>
@@ -9212,9 +9212,9 @@ LEFT JOIN LATERAL (
       <![CDATA[
 LogicalProject(A=[$0], TS=[$1], X=[$2], X0=[$4])
   LogicalJoin(condition=[=($3, $5)], joinType=[left])
-    LogicalProject(EXPR$0=[$0], EXPR$1=[$1], X=[$4], 
EXPR$00=[CAST($0):VARCHAR(20) NOT NULL])
-      LogicalJoin(condition=[AND(=($1, $5), =($2, $6))], joinType=[left])
-        LogicalProject(EXPR$0=[$0], EXPR$1=[$1], EXPR$00=[CAST($0):VARCHAR(20) 
NOT NULL], EXPR$10=[$1])
+    LogicalProject(EXPR$0=[$0], EXPR$1=[$1], X=[$3], 
EXPR$00=[CAST($0):VARCHAR(20) NOT NULL])
+      LogicalJoin(condition=[AND(=($1, $4), =($2, $5))], joinType=[left])
+        LogicalProject(EXPR$0=[$0], EXPR$1=[$1], EXPR$00=[CAST($0):VARCHAR(20) 
NOT NULL])
           LogicalValues(tuples=[[{ 'a', 1 }]])
         LogicalFilter(condition=[<=($3, 1)])
           LogicalProject(X=[$2], EXPR$1=[$3], EXPR$00=[$4], rn=[ROW_NUMBER() 
OVER (PARTITION BY $3, $4)])
diff --git 
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml 
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index c36f7ad59b..aafc9c11ef 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -1770,15 +1770,15 @@ LogicalAggregate(group=[{}], EXPR$0=[COUNT()], 
EXPR$1=[SUM($0)])
     </Resource>
     <Resource name="plan">
       <![CDATA[
-LogicalProject(C=[$0], D=[$1], C0=[$4], F=[$5])
-  LogicalJoin(condition=[AND(=($0, $6), =($3, $7))], joinType=[inner])
-    LogicalProject(C=[$0], D=[$1], C0=[$0], $f3=[+($0, $1)])
+LogicalProject(C=[$0], D=[$1], C0=[$3], F=[$4])
+  LogicalJoin(condition=[AND(=($0, $5), =($2, $6))], joinType=[inner])
+    LogicalProject(C=[$0], D=[$1], $f2=[+($0, $1)])
       LogicalValues(tuples=[[{ 4, 5 }]])
-    LogicalProject(C=[$1], F=[*($0, $1)], C2=[$1], $f3=[$2])
+    LogicalProject(C=[$1], F=[*($0, $1)], C2=[$1], $f2=[$2])
       LogicalJoin(condition=[=($2, *($0, $1))], joinType=[inner])
         LogicalValues(tuples=[[{ 2 }]])
         LogicalAggregate(group=[{0, 1}])
-          LogicalProject(C=[$0], $f3=[+($0, $1)])
+          LogicalProject(C=[$0], $f2=[+($0, $1)])
             LogicalValues(tuples=[[{ 4, 5 }]])
 ]]>
     </Resource>
@@ -1796,18 +1796,18 @@ cross join lateral
     </Resource>
     <Resource name="plan">
       <![CDATA[
-LogicalProject(C=[$0], N=[$3])
-  LogicalJoin(condition=[AND(=($0, $4), =($2, $5))], joinType=[inner])
-    LogicalProject(C=[$0], C0=[$0], $f2=[+($0, 1)])
+LogicalProject(C=[$0], N=[$2])
+  LogicalJoin(condition=[AND(=($0, $3), =($1, $4))], joinType=[inner])
+    LogicalProject(C=[$0], $f1=[+($0, 1)])
       LogicalValues(tuples=[[{ 4 }]])
-    LogicalProject(N=[+($0, $1)], EXPR$0=[+($0, $1)], $f2=[$2])
+    LogicalProject(N=[+($0, $1)], EXPR$0=[+($0, $1)], $f1=[$2])
       LogicalJoin(condition=[=($0, $1)], joinType=[inner])
         LogicalValues(tuples=[[{ 2 }]])
-        LogicalProject(EXPR$0=[$1], $f2=[$1])
+        LogicalProject(EXPR$0=[$1], $f1=[$1])
           LogicalJoin(condition=[true], joinType=[inner])
             LogicalValues(tuples=[[{ 3 }]])
             LogicalAggregate(group=[{0}])
-              LogicalProject($f2=[+($0, 1)])
+              LogicalProject($f1=[+($0, 1)])
                 LogicalValues(tuples=[[{ 4 }]])
 ]]>
     </Resource>
@@ -5222,9 +5222,9 @@ LogicalProject(C=[$0], D=[$1], C0=[$2])
     </Resource>
     <Resource name="plan">
       <![CDATA[
-LogicalProject(C=[$0], D=[$1], D0=[$4], C0=[$5])
-  LogicalJoin(condition=[AND(=($0, $6), =($2, $5))], joinType=[left])
-    LogicalProject(C=[$0], D=[$1], $f2=[+($0, $1)], C0=[$0])
+LogicalProject(C=[$0], D=[$1], D0=[$3], C0=[$4])
+  LogicalJoin(condition=[AND(=($0, $5), =($2, $4))], joinType=[left])
+    LogicalProject(C=[$0], D=[$1], $f2=[+($0, $1)])
       LogicalValues(tuples=[[{ 4, 5 }]])
     LogicalProject(C=[$1], EXPR$1=[*($0, $1)], C2=[$1])
       LogicalJoin(condition=[true], joinType=[inner])
@@ -5247,18 +5247,18 @@ as r(n) on c=n]]>
     </Resource>
     <Resource name="plan">
       <![CDATA[
-LogicalProject(C=[$0], N=[$3])
-  LogicalJoin(condition=[AND(=($0, $3), =($2, $4))], joinType=[left])
-    LogicalProject(C=[$0], C0=[$0], $f2=[+($0, 1)])
+LogicalProject(C=[$0], N=[$2])
+  LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[left])
+    LogicalProject(C=[$0], $f1=[+($0, 1)])
       LogicalValues(tuples=[[{ 4 }]])
-    LogicalProject(EXPR$0=[+($0, $1)], $f2=[$2])
+    LogicalProject(EXPR$0=[+($0, $1)], $f1=[$2])
       LogicalJoin(condition=[=($0, $1)], joinType=[inner])
         LogicalValues(tuples=[[{ 2 }]])
-        LogicalProject(EXPR$0=[$1], $f2=[$1])
+        LogicalProject(EXPR$0=[$1], $f1=[$1])
           LogicalJoin(condition=[true], joinType=[inner])
             LogicalValues(tuples=[[{ 3 }]])
             LogicalAggregate(group=[{0}])
-              LogicalProject($f2=[+($0, 1)])
+              LogicalProject($f1=[+($0, 1)])
                 LogicalValues(tuples=[[{ 4 }]])
 ]]>
     </Resource>
diff --git a/core/src/test/resources/sql/lateral.iq 
b/core/src/test/resources/sql/lateral.iq
index 5c82727b89..4c4ffbe170 100644
--- a/core/src/test/resources/sql/lateral.iq
+++ b/core/src/test/resources/sql/lateral.iq
@@ -244,4 +244,98 @@ where job = 'MANAGER';
 
 !ok
 
+# 3 test cases for [CALCITE-7646] CorrelateProjectExtractor
+# does not handle nested field accesses cor0.field0.field1.
+
+# All queries use LATERAL, which converts directly to a Correlate.
+# The results were validated on Postgres
+
+!use scott
+
+select t.dd, t.x
+from dept d,
+lateral (select d.deptno as dd, u.x
+         from unnest(array[d.deptno + 100]) as u(x)) as t
+where d.dname = 'SALES';
++----+-----+
+| DD | X   |
++----+-----+
+| 30 | 130 |
++----+-----+
+(1 row)
+
+!ok
+
+select t.dd, t.dd1, t.x
+from dept d,
+lateral (select d.deptno as dd, d.deptno + 1 as dd1, u.x
+         from unnest(array[1, 2]) as u(x)) as t
+where d.dname = 'SALES';
++----+-----+---+
+| DD | DD1 | X |
++----+-----+---+
+| 30 |  31 | 1 |
+| 30 |  31 | 2 |
++----+-----+---+
+(2 rows)
+
+!ok
+!if (use_old_decorr) {
+# The correlated computation d.deptno + 1 (DD1) has been extracted into the 
left
+# input of the EnumerableNestedLoopJoin, as $f3. The right input,
+# UNNEST(ARRAY[1, 2]), references no correlation variable, so decorrelation
+# replaces the Correlate with a join.
+EnumerableCalc(expr#0..4=[{inputs}], proj#0..2=[{exprs}])
+  EnumerableHashJoin(condition=[AND(=($3, $5), =($4, $6))], joinType=[semi])
+    EnumerableCalc(expr#0..2=[{inputs}], proj#0..2=[{exprs}], DEPTNO=[$t0], 
$f3=[$t1])
+      EnumerableNestedLoopJoin(condition=[true], joinType=[inner])
+        EnumerableCalc(expr#0..2=[{inputs}], expr#3=[1], expr#4=[+($t0, $t3)], 
expr#5=['SALES':VARCHAR(14)], expr#6=[=($t1, $t5)], DEPTNO=[$t0], $f3=[$t4], 
$condition=[$t6])
+          EnumerableTableScan(table=[[scott, DEPT]])
+        EnumerableUncollect
+          EnumerableCalc(expr#0=[{inputs}], expr#1=[1], expr#2=[2], 
expr#3=[ARRAY($t1, $t2)], EXPR$0=[$t3])
+            EnumerableValues(tuples=[[{ 0 }]])
+    EnumerableCalc(expr#0..2=[{inputs}], expr#3=[1], expr#4=[+($t0, $t3)], 
expr#5=['SALES':VARCHAR(14)], expr#6=[=($t1, $t5)], DEPTNO=[$t0], $f3=[$t4], 
$condition=[$t6])
+      EnumerableTableScan(table=[[scott, DEPT]])
+!plan
+!}
+
+# COALESCE(d.path, ARRAY[CAST(NULL AS INTEGER)]) converts to
+# CASE(IS NOT NULL($cor0.PATH), $cor0.PATH, ARRAY(null:INTEGER)). The constant
+# ARRAY(null:INTEGER) operand must not prevent extracting the CASE to the
+# left input of the Correlate operator
+select d.deptno, t.x
+from (select deptno,
+        case when deptno = 10 then array[deptno, deptno + 1] end as path
+      from dept) as d,
+lateral (select * from unnest(coalesce(d.path, array[cast(null as integer)])) 
as u(x)) as t
+order by d.deptno, t.x;
++--------+----+
+| DEPTNO | X  |
++--------+----+
+|     10 | 10 |
+|     10 | 11 |
+|     20 |    |
+|     30 |    |
+|     40 |    |
++--------+----+
+(5 rows)
+
+!ok
+!if (use_old_decorr) {
+# The entire CASE produced by COALESCE has been extracted into the left input 
of the
+# EnumerableCorrelate, as $f2.  The right input reads the array through 
$cor0.$f2.
+# The query cannot be decorrelated because of the remaining Uncollect.
+EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], dir1=[ASC])
+  EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0], X=[$t2])
+    EnumerableCorrelate(correlation=[$cor0], joinType=[inner], 
requiredColumns=[{1}])
+      EnumerableCalc(expr#0..2=[{inputs}], expr#3=[CAST($t0):INTEGER NOT 
NULL], expr#4=[10], expr#5=[=($t3, $t4)], expr#6=[1], expr#7=[+($t0, $t6)], 
expr#8=[ARRAY($t3, $t7)], expr#9=[null:INTEGER NOT NULL ARRAY], 
expr#10=[CASE($t5, $t8, $t9)], expr#11=[IS NOT NULL($t10)], 
expr#12=[CAST($t10):INTEGER NOT NULL ARRAY NOT NULL], 
expr#13=[CAST($t12):INTEGER ARRAY NOT NULL], expr#14=[null:INTEGER], 
expr#15=[ARRAY($t14)], expr#16=[CASE($t11, $t13, $t15)], DEPTNO=[$t0], 
$f2=[$t16])
+        EnumerableTableScan(table=[[scott, DEPT]])
+      EnumerableUncollect
+        EnumerableCalc(expr#0=[{inputs}], expr#1=[$cor0], expr#2=[$t1.$f2], 
EXPR$0=[$t2])
+          EnumerableValues(tuples=[[{ 0 }]])
+!plan
+!}
+
+!set planner-rules original
+
 # End lateral.iq


Reply via email to