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 20310a95c9e6..caa9b4d4dedf 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 52c1875e3696..f2047dcea538 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
+ * [CALCITE-7487]
+ * ProjectJoinTransposeRule throws ArrayIndexOutOfBoundsException in
+ * PushProjector when a Join input has a zero-column row type. */
+ @Test void testProjectJoinTransposeWithZeroColumnRightInput() {
+ relFn(b -> zeroColumnJoinInputRelFn(b, false))
+ .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+ }
+
+ /** Test case for
+ * [CALCITE-7487]
+ * ProjectJoinTransposeRule throws ArrayIndexOutOfBoundsException in
+ * PushProjector when a Join input has a zero-column row type. */
+ @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 e2b725a7d66d..b4e3878b9a74 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -12359,6 +12359,44 @@ LogicalProject(DEPTNO=[$0])
LogicalAggregate(group=[{}], DUMMY=[COUNT()])
LogicalProject(EMPNO=[$0])
LogicalTableScan(table=[[scott, EMP]])
+]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+