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]]) +]]> + + + + + + + + + + + + + + + +