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 2ed8b0ca75 [CALCITE-7487] ProjectJoinTransposeRule throws 
ArrayIndexOutOfBoundsException in PushProjector when a Join input has a 
zero-column row type
2ed8b0ca75 is described below

commit 2ed8b0ca757372db83826aa9d7092967e688643a
Author: microbluey <[email protected]>
AuthorDate: Tue Aug 11 13:49:41 2026 +0800

    [CALCITE-7487] ProjectJoinTransposeRule throws 
ArrayIndexOutOfBoundsException in PushProjector when a Join input has a 
zero-column row type
    
    PushProjector.locateAllRefs contains a workaround, originally added for
    Fennel, that arbitrarily projects the first column of a Join or SetOp
    input when nothing else is projected from it. The workaround assumes the
    input has a first column to fall back on.
    
    That assumption does not hold for a zero-column input: a Values with an
    empty row type and a single empty tuple returns one row with zero
    columns and is the identity for cross join. It arises when an Aggregate
    with GROUP BY () has its output pruned to zero columns. In that case the
    workaround sets a bit that points past the input's fields, and
    createProjectRefsAndExprs later uses that bit to index into an empty
    field list, throwing ArrayIndexOutOfBoundsException.
    
    Guard both the left and the right workaround on the corresponding input
    having at least one field, so that the workaround is skipped rather than
    producing an out-of-range reference. Each guard tests the field count of
    the input it protects: nFields for the left, nFieldsRight for the right.
    
    Both directions crash, so add a regression test for each.
---
 .../apache/calcite/rel/rules/PushProjector.java    | 15 +++++--
 .../org/apache/calcite/test/RelOptRulesTest.java   | 50 ++++++++++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 38 ++++++++++++++++
 3 files changed, 100 insertions(+), 3 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java 
b/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
index 20310a95c9..caa9b4d4de 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/PushProjector.java
@@ -457,13 +457,22 @@ public boolean locateAllRefs() {
         || (childRel instanceof SetOp)) {
       // if nothing is projected from the children, arbitrarily project
       // the first columns; this is necessary since Fennel doesn't
-      // handle 0-column projections
-      if (nProject == 0 && childPreserveExprs.isEmpty()) {
+      // handle 0-column projections.
+      //
+      // An input may legitimately have a zero-column row type: for
+      // example, a Values with an empty row type and a single empty
+      // tuple, which returns one row with zero columns and is the
+      // identity for cross join. There is no first column to fall back
+      // on in that case, so skip the workaround rather than set a bit
+      // that points past the input's fields; createProjectRefsAndExprs
+      // would use it to index into an empty field list.
+      if (nProject == 0 && childPreserveExprs.isEmpty() && nFields > 0) {
         projRefs.set(0);
         nProject = 1;
       }
       if (childRel instanceof Join) {
-        if (nRightProject == 0 && rightPreserveExprs.isEmpty()) {
+        if (nRightProject == 0 && rightPreserveExprs.isEmpty()
+            && nFieldsRight > 0) {
           projRefs.set(nFields);
           nRightProject = 1;
         }
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java 
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index ca5cc42385..06ee61c26c 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -1559,6 +1559,56 @@ private void 
checkSemiOrAntiJoinProjectTranspose(JoinRelType type) {
     checkJoinProjectTransposeDoesNotMatch(JoinRelType.LEFT_MARK);
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7487";>[CALCITE-7487]
+   * ProjectJoinTransposeRule throws ArrayIndexOutOfBoundsException in
+   * PushProjector when a Join input has a zero-column row type</a>. */
+  @Test void testProjectJoinTransposeWithZeroColumnRightInput() {
+    relFn(b -> zeroColumnJoinInputRelFn(b, false))
+        .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7487";>[CALCITE-7487]
+   * ProjectJoinTransposeRule throws ArrayIndexOutOfBoundsException in
+   * PushProjector when a Join input has a zero-column row type</a>. */
+  @Test void testProjectJoinTransposeWithZeroColumnLeftInput() {
+    relFn(b -> zeroColumnJoinInputRelFn(b, true))
+        .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+  }
+
+  /** Builds {@code Project(CAST(col1))} over a cross join in which one input 
is
+   * DEE -- a {@link org.apache.calcite.rel.core.Values} with an empty row 
type,
+   * the identity for cross join. The project must be non-identity, otherwise
+   * {@link RelBuilder} collapses it away and the rule never fires. */
+  private static RelNode zeroColumnJoinInputRelFn(RelBuilder b,
+      boolean deeOnLeft) {
+    final RelDataTypeFactory typeFactory = b.getTypeFactory();
+    final RelDataType bigintType =
+        typeFactory.createSqlType(SqlTypeName.BIGINT);
+    final RelNode nonEmpty = b
+        .values(
+            ImmutableList.of(
+                ImmutableList.of(
+                    (RexLiteral) 
b.getRexBuilder().makeZeroLiteral(bigintType))),
+            typeFactory.builder().add("col1", bigintType).build())
+        .build();
+    final RelNode dee = b
+        .values(ImmutableList.of(ImmutableList.of()),
+            typeFactory.builder().build())
+        .build();
+    final RelDataType varcharType =
+        typeFactory.createSqlType(SqlTypeName.VARCHAR);
+    return b
+        .push(deeOnLeft ? dee : nonEmpty)
+        .push(deeOnLeft ? nonEmpty : dee)
+        .join(JoinRelType.INNER, b.literal(true))
+        // DEE contributes no fields, so the sole column is at index 0
+        // whichever side it is on.
+        .project(b.getRexBuilder().makeCast(varcharType, b.field(0)))
+        .build();
+  }
+
   /** A SEMI, ANTI or LEFT_MARK join does not project its right input, so
    * {@link JoinProjectTransposeRule} must not pull projects above it. */
   private void checkJoinProjectTransposeDoesNotMatch(JoinRelType type) {
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 0b544e3f1c..49681f45a3 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -12480,6 +12480,44 @@ LogicalProject(DEPTNO=[$0])
     LogicalAggregate(group=[{}], DUMMY=[COUNT()])
       LogicalProject(EMPNO=[$0])
         LogicalTableScan(table=[[scott, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testProjectJoinTransposeWithZeroColumnLeftInput">
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(col1=[CAST($0):VARCHAR NOT NULL])
+  LogicalJoin(condition=[true], joinType=[inner])
+    LogicalValues(tuples=[[{  }]])
+    LogicalValues(tuples=[[{ 0 }]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalJoin(condition=[true], joinType=[inner])
+  LogicalProject
+    LogicalValues(tuples=[[{  }]])
+  LogicalProject(col1=[CAST($0):VARCHAR NOT NULL])
+    LogicalValues(tuples=[[{ 0 }]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testProjectJoinTransposeWithZeroColumnRightInput">
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(col1=[CAST($0):VARCHAR NOT NULL])
+  LogicalJoin(condition=[true], joinType=[inner])
+    LogicalValues(tuples=[[{ 0 }]])
+    LogicalValues(tuples=[[{  }]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalJoin(condition=[true], joinType=[inner])
+  LogicalProject(col1=[CAST($0):VARCHAR NOT NULL])
+    LogicalValues(tuples=[[{ 0 }]])
+  LogicalProject
+    LogicalValues(tuples=[[{  }]])
 ]]>
     </Resource>
   </TestCase>

Reply via email to