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 b4d50a0772 [CALCITE-7488] ProjectJoinTransposeRule produces row-type
mismatch when pushing a compound expression containing a nullability-narrowing
CAST through an outer Join
b4d50a0772 is described below
commit b4d50a077256d192c95857335b5ea88d91d8870d
Author: Mihai Budiu <[email protected]>
AuthorDate: Fri Aug 21 18:06:54 2026 -0700
[CALCITE-7488] ProjectJoinTransposeRule produces row-type mismatch when
pushing a compound expression containing a nullability-narrowing CAST through
an outer Join
Signed-off-by: Mihai Budiu <[email protected]>
---
.../apache/calcite/rel/rules/PushProjector.java | 11 +--
.../org/apache/calcite/test/RelOptRulesTest.java | 63 +++++++++++++++++
.../org/apache/calcite/test/RelOptRulesTest.xml | 80 ++++++++++++++++++++++
3 files changed, 150 insertions(+), 4 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 caa9b4d4de..9dc5172896 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
@@ -726,14 +726,17 @@ private static class InputSpecialOpFinder extends
RexVisitorImpl<Void> {
return null;
}
- private boolean isStrong(final ImmutableBitSet exprArgs, final RexNode
call) {
+ private boolean canPush(final ImmutableBitSet exprArgs, final RexNode
call) {
// If the expressions do not use any of the inputs that require output
to be null,
// no need to check. Otherwise, check that the expression is null.
// For example, in an "left outer join", we don't require that
expressions
// pushed down into the left input to be strong. On the other hand,
// expressions pushed into the right input must be. In that case,
// strongFields == right input fields.
- return !strongFields.intersects(exprArgs) || strong.isNull(call);
+ if (!strongFields.intersects(exprArgs)) {
+ return true;
+ }
+ return strong.isNull(call) && call.getType().isNullable();
}
private boolean preserve(RexNode call) {
@@ -743,13 +746,13 @@ private boolean preserve(RexNode call) {
// it only references expressions on the right
final ImmutableBitSet exprArgs = RelOptUtil.InputFinder.bits(call);
if (exprArgs.cardinality() > 0) {
- if (leftFields.contains(exprArgs) && isStrong(exprArgs, call)) {
+ if (leftFields.contains(exprArgs) && canPush(exprArgs, call)) {
if (!preserveLeft.contains(call)) {
preserveLeft.add(call);
}
return true;
} else if (requireNonNull(rightFields,
"rightFields").contains(exprArgs)
- && isStrong(exprArgs, call)) {
+ && canPush(exprArgs, call)) {
requireNonNull(preserveRight, "preserveRight");
if (!preserveRight.contains(call)) {
preserveRight.add(call);
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 06ee61c26c..76ff0a8313 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -1609,6 +1609,69 @@ private static RelNode
zeroColumnJoinInputRelFn(RelBuilder b,
.build();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7488">[CALCITE-7488]
+ * ProjectJoinTransposeRule produces row-type mismatch when pushing a
compound
+ * expression containing a nullability-narrowing CAST through an outer
+ * Join</a>. */
+ @Test void testProjectJoinTransposeNarrowingCastInCompoundExpr() {
+ relFn(b -> castInCaseRelFn(b, JoinRelType.LEFT, true))
+ .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+ }
+
+ /** As {@link #testProjectJoinTransposeNarrowingCastInCompoundExpr()}, but
the
+ * null-generating input of the join is the left one. */
+ @Test void testProjectJoinTransposeNarrowingCastInCompoundExprRightJoin() {
+ relFn(b -> castInCaseRelFn(b, JoinRelType.RIGHT, true))
+ .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+ }
+
+ /** As {@link #testProjectJoinTransposeNarrowingCastInCompoundExpr()}, but
both
+ * inputs of the join are null-generating. */
+ @Test void testProjectJoinTransposeNarrowingCastInCompoundExprFullJoin() {
+ relFn(b -> castInCaseRelFn(b, JoinRelType.FULL, true))
+ .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+ }
+
+ /** Without the narrowing casts the expression has a nullable type, so
pushing
+ * it into the null-generating input does not change its type, and the rule
+ * still pushes it. */
+ @Test void testProjectJoinTransposeNullableCompoundExpr() {
+ relFn(b -> castInCaseRelFn(b, JoinRelType.LEFT, false))
+ .withRule(CoreRules.PROJECT_JOIN_TRANSPOSE).check();
+ }
+
+ /** Builds {@code Project(CASE(DNAME IS NOT NULL, DNAME, LOC))} over an outer
+ * join of EMP and DEPT, with DEPT on the null-generating side. The CASE is
+ * null whenever DEPT's columns are null, so it is a candidate for being
pushed
+ * into the DEPT input. */
+ private static RelNode castInCaseRelFn(RelBuilder b, JoinRelType joinType,
+ boolean narrowing) {
+ final RexBuilder rb = b.getRexBuilder();
+ if (joinType == JoinRelType.RIGHT) {
+ b.scan("DEPT").scan("EMP");
+ } else {
+ b.scan("EMP").scan("DEPT");
+ }
+ b.join(joinType,
+ b.equals(b.field(2, 0, "DEPTNO"), b.field(2, 1, "DEPTNO")));
+ RexNode dname = b.field("DEPT", "DNAME");
+ RexNode loc = b.field("DEPT", "LOC");
+ if (narrowing) {
+ dname = rb.makeCast(notNullType(b, dname), dname, false, false);
+ loc = rb.makeCast(notNullType(b, loc), loc, false, false);
+ }
+ return b.project(
+ b.call(SqlStdOperatorTable.CASE,
+ b.call(SqlStdOperatorTable.IS_NOT_NULL, b.field("DEPT",
"DNAME")),
+ dname, loc))
+ .build();
+ }
+
+ private static RelDataType notNullType(RelBuilder b, RexNode e) {
+ return b.getTypeFactory().createTypeWithNullability(e.getType(), false);
+ }
+
/** 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 96ca66bbc7..33d72da4d3 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -12436,6 +12436,86 @@ LogicalProject(EXPR$0=[$1], EXPR$1=[$3])
LogicalProject($f1=[$1], EXPR$0=[ITEM($0, 0)])
LogicalProject(C_NATIONKEY=[$0], $f1=[ITEM($0, 0)])
LogicalTableScan(table=[[CATALOG, SALES, CUSTOMER]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testProjectJoinTransposeNarrowingCastInCompoundExpr">
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($9), CAST($9):VARCHAR(14) NOT NULL,
CAST($10):VARCHAR(13) NOT NULL)])
+ LogicalJoin(condition=[=($7, $8)], joinType=[left])
+ LogicalTableScan(table=[[scott, EMP]])
+ LogicalTableScan(table=[[scott, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($2), CAST($2):VARCHAR(14) NOT NULL,
CAST($3):VARCHAR(13) NOT NULL)])
+ LogicalJoin(condition=[=($0, $1)], joinType=[left])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[scott, EMP]])
+ LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2])
+ LogicalTableScan(table=[[scott, DEPT]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testProjectJoinTransposeNarrowingCastInCompoundExprFullJoin">
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($9), CAST($9):VARCHAR(14) NOT NULL,
CAST($10):VARCHAR(13) NOT NULL)])
+ LogicalJoin(condition=[=($7, $8)], joinType=[full])
+ LogicalTableScan(table=[[scott, EMP]])
+ LogicalTableScan(table=[[scott, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($2), CAST($2):VARCHAR(14) NOT NULL,
CAST($3):VARCHAR(13) NOT NULL)])
+ LogicalJoin(condition=[=($0, $1)], joinType=[full])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[scott, EMP]])
+ LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2])
+ LogicalTableScan(table=[[scott, DEPT]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase
name="testProjectJoinTransposeNarrowingCastInCompoundExprRightJoin">
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($1), CAST($1):VARCHAR(14) NOT NULL,
CAST($2):VARCHAR(13) NOT NULL)])
+ LogicalJoin(condition=[=($0, $10)], joinType=[right])
+ LogicalTableScan(table=[[scott, DEPT]])
+ LogicalTableScan(table=[[scott, EMP]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($1), CAST($1):VARCHAR(14) NOT NULL,
CAST($2):VARCHAR(13) NOT NULL)])
+ LogicalJoin(condition=[=($0, $3)], joinType=[right])
+ LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2])
+ LogicalTableScan(table=[[scott, DEPT]])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[scott, EMP]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testProjectJoinTransposeNullableCompoundExpr">
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject($f0=[CASE(IS NOT NULL($9), $9, $10)])
+ LogicalJoin(condition=[=($7, $8)], joinType=[left])
+ LogicalTableScan(table=[[scott, EMP]])
+ LogicalTableScan(table=[[scott, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalProject($f0=[$2])
+ LogicalJoin(condition=[=($0, $1)], joinType=[left])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[scott, EMP]])
+ LogicalProject(DEPTNO=[$0], $f0=[CASE(IS NOT NULL($1), $1, $2)])
+ LogicalTableScan(table=[[scott, DEPT]])
]]>
</Resource>
</TestCase>