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>

Reply via email to