Repository: apache/calcite
Pull request #4350: [CALCITE-5387] Type-mismatch on nullability in JoinPushTransitivePredicatesRule RelRule
Author: login: xiedeyantu
Labels: name: LGTM-will-merge-soon
description: Overall PR looks OK. Only minor things left.
Created: 2025-04-30T04:30:08Z | Merged: 2025-05-03T22:44:43Z



Diff:
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPredicates.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPredicates.java
index cfdc37239259..9dcfb8e92e39 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPredicates.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdPredicates.java
@@ -803,12 +803,12 @@ public RelOptPredicateList inferPredicates(
           Mappings.createShiftMapping(nSysFields + nFieldsLeft + nFieldsRight,
               0, nSysFields + nFieldsLeft, nFieldsRight);
       final RexPermuteInputsShuttle rightPermute =
-          new RexPermuteInputsShuttle(rightMapping, joinRel);
+          new RexPermuteInputsShuttle(rightMapping, true, joinRel.getRight());
       Mappings.TargetMapping leftMapping =
           Mappings.createShiftMapping(nSysFields + nFieldsLeft, 0, nSysFields,
               nFieldsLeft);
       final RexPermuteInputsShuttle leftPermute =
-          new RexPermuteInputsShuttle(leftMapping, joinRel);
+          new RexPermuteInputsShuttle(leftMapping, true, joinRel.getLeft());
       final List<RexNode> leftInferredPredicates = new ArrayList<>();
       final List<RexNode> rightInferredPredicates = new ArrayList<>();
 
diff --git a/core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java b/core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
index 9f418b1b7241..0906a6610da7 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
@@ -37,6 +37,7 @@ public class RexPermuteInputsShuttle extends RexShuttle {
 
   private final Mappings.TargetMapping mapping;
   private final ImmutableList<RelDataTypeField> fields;
+  private final boolean matchTargetType;
 
   //~ Constructors -----------------------------------------------------------
 
@@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle.
+   *
+   * <p>The mapping provides at most one target for every source.
+   *
+   * @param mapping               Mapping
+   * @param shouldMatchTargetType If {@code false}, each input reference is substituted
+   *                              with a reference retaining the original input's type.
+   *                              If {@code true}, each input reference is substituted
+   *                              with a reference matching the target type.
+   * @param inputs                Input relational expressions
+   */
+  public RexPermuteInputsShuttle(
+      Mappings.TargetMapping mapping,
+      boolean shouldMatchTargetType,
+      RelNode... inputs) {
+    this(mapping, fields(inputs), shouldMatchTargetType);
   }
 
   private RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       ImmutableList<RelDataTypeField> fields) {
+    this(mapping, fields, false);
+  }
+
+  private RexPermuteInputsShuttle(
+      Mappings.TargetMapping mapping,
+      ImmutableList<RelDataTypeField> fields,
+      boolean matchTargetType) {
     this.mapping = mapping;
     this.fields = fields;
+    this.matchTargetType = matchTargetType;
   }
 
   /** Creates a shuttle with an empty field list. It cannot handle GET calls but
@@ -85,9 +113,15 @@ private static ImmutableList<RelDataTypeField> fields(RelNode[] inputs) {
   @Override public RexNode visitInputRef(RexInputRef local) {
     final int index = local.getIndex();
     int target = mapping.getTarget(index);
-    return new RexInputRef(
-        target,
-        local.getType());
+    if (!matchTargetType) {
+      return new RexInputRef(
+          target,
+          local.getType());
+    } else {
+      return new RexInputRef(
+          target,
+          fields.get(target).getType());
+    }
   }
 
   @Override public RexNode visitCall(RexCall 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 65c9f417231b..3278dab3e5ef 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -6896,6 +6896,25 @@ private HepProgram getTransitiveProgram() {
         .checkUnchanged();
   }
 
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-5387">[CALCITE-5387]
+   * Type-mismatch on nullability in JoinPushTransitivePredicatesRule RelRule</a>. */
+  @Test void testJoinPushTransitivePredicatesNullabilityIssue() {
+    final String sql = "WITH\n"
+        + "non_null_table AS (\n"
+        + "  SELECT DATE '2023-08-07' AS date_col_non_null FROM dept\n"
+        + "),\n"
+        + "null_table AS (\n"
+        + "  SELECT CAST(null as DATE) AS date_col_null FROM dept\n"
+        + ")\n"
+        + "SELECT *\n"
+        + "FROM non_null_table\n"
+        + "JOIN null_table\n"
+        + "ON null_table.date_col_null = non_null_table.date_col_non_null";
+
+    sql(sql).withRule(CoreRules.JOIN_PUSH_TRANSITIVE_PREDICATES).check();
+  }
+
   /** Test case of
    * <a href="https://issues.apache.org/jira/browse/CALCITE-6432">[CALCITE-6432]
    * Infinite loop for JoinPushTransitivePredicatesRule</a>. */
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 1a4a98bbb8d0..37915af90586 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -7468,6 +7468,41 @@ LogicalProject(DEPTNO=[$0], NAME=[$1], R=[$3], EXPR$1=[$4])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
       LogicalProject(R=[RANK() OVER (ORDER BY $1)], EXPR$1=[+(1, 1)])
         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testJoinPushTransitivePredicatesNullabilityIssue">
+    <Resource name="sql">
+      <![CDATA[WITH
+non_null_table AS (
+  SELECT DATE '2023-08-07' AS date_col_non_null FROM dept
+),
+null_table AS (
+  SELECT CAST(null as DATE) AS date_col_null FROM dept
+)
+SELECT *
+FROM non_null_table
+JOIN null_table
+ON null_table.date_col_null = non_null_table.date_col_non_null]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(DATE_COL_NON_NULL=[$0], DATE_COL_NULL=[$1])
+  LogicalJoin(condition=[=($1, $0)], joinType=[inner])
+    LogicalProject(DATE_COL_NON_NULL=[2023-08-07])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+    LogicalProject(DATE_COL_NULL=[null:DATE])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(DATE_COL_NON_NULL=[$0], DATE_COL_NULL=[$1])
+  LogicalJoin(condition=[=($1, $0)], joinType=[inner])
+    LogicalValues(tuples=[[]])
+    LogicalFilter(condition=[=($0, 2023-08-07)])
+      LogicalProject(DATE_COL_NULL=[null:DATE])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
     </Resource>
   </TestCase>


Reviews:
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:07:37Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:09:22Z
user: login: xiedeyantu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T07:51:59Z
user: login: mihaibudiu
body: Only some documentation improvements are required.
I have thus approved, but will wait for these to happen to merge.
Since this is a small PR you can probably also squash the commits after you make the documentation changes.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T17:37:46Z
user: login: xiedeyantu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T23:53:43Z
user: login: mihaibudiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T00:26:05Z
user: login: xiedeyantu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T00:35:59Z

Review comments:
diff_hunk: @@ -54,14 +55,29 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  public RexPermuteInputsShuttle(
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: mihaibudiu
body: can you please document the new flag using JavaDoc?
created_at: 2025-04-30T07:07:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,9 +101,15 @@ private static ImmutableList<RelDataTypeField> fields(RelNode[] inputs) {
   @Override public RexNode visitInputRef(RexInputRef local) {
     final int index = local.getIndex();
     int target = mapping.getTarget(index);
-    return new RexInputRef(
-        target,
-        local.getType());
+    if (!isStrictTypeMatch) {
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: mihaibudiu
body: this doesn't look to me like it's a "strict" comparison
basically you are copying types from different fields: source, or destination
Am I right?
created_at: 2025-04-30T07:09:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,9 +101,15 @@ private static ImmutableList<RelDataTypeField> fields(RelNode[] inputs) {
   @Override public RexNode visitInputRef(RexInputRef local) {
     final int index = local.getIndex();
     int target = mapping.getTarget(index);
-    return new RexInputRef(
-        target,
-        local.getType());
+    if (!isStrictTypeMatch) {
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: xiedeyantu
body: Yes, maybe I need to change the name `shouldMatchTargetType`? This affects the creation of subsequent Filters in the rule.
created_at: 2025-04-30T07:51:59Z
author_association: MEMBER
diff_hunk: @@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle with target type matching control.
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: mihaibudiu
body: what is 'control'? It's not mentioned anywhere else.
created_at: 2025-04-30T17:33:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle with target type matching control.
+   *
+   * <p>The mapping provides at most one target for every source. If a source
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: mihaibudiu
body: I think this paragraph belongs in the documentation for the mapping parameter.
I don't think you should discuss here what TargetMapping does, that should belong to the documentation of TargetMapping itself. What you should say is what this function assumes about the mapping. Perhaps you mean to say that every RexInputRef in the expression that will be analyzed by this shuttle needs to be a key in this mapping?
created_at: 2025-04-30T17:34:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle with target type matching control.
+   *
+   * <p>The mapping provides at most one target for every source. If a source
+   * has no targets and is referenced in the expression,
+   * {@link org.apache.calcite.util.mapping.Mappings.TargetMapping#getTarget(int)}
+   * will give an error. Otherwise the mapping gives a unique target.
+   *
+   * @param mapping          Mapping that defines source-to-target relation
+   * @param shouldMatchTargetType If true, enforces use target type when remapping inputs
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: mihaibudiu
body: Based on this description, this should be called 'matchTargetType'. There is no need for 'should'.
I would describe what this does for both 'true' and 'false' cases.
In the 'false' case each input ref is substituted with a reference with the same type as the original input.
In the 'true' case each input ref is substituted with a reference with the same type as the target type.
created_at: 2025-04-30T17:36:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle with target type matching control.
+   *
+   * <p>The mapping provides at most one target for every source. If a source
+   * has no targets and is referenced in the expression,
+   * {@link org.apache.calcite.util.mapping.Mappings.TargetMapping#getTarget(int)}
+   * will give an error. Otherwise the mapping gives a unique target.
+   *
+   * @param mapping          Mapping that defines source-to-target relation
+   * @param shouldMatchTargetType If true, enforces use target type when remapping inputs
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: xiedeyantu
body: I modified the description of `matchTargetType` according to your suggestion, and kept the other descriptions unchanged. Do you think it's ok?
created_at: 2025-04-30T23:53:43Z
author_association: MEMBER
diff_hunk: @@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle with target type matching control.
+   *
+   * <p>The mapping provides at most one target for every source. If a source
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: mihaibudiu
body: what I suggested here is that the second phrase and third should be removed. (If a source... unique target). They do not document what this method does, they document what getTarget does.
created_at: 2025-05-01T00:26:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,14 +55,41 @@ public class RexPermuteInputsShuttle extends RexShuttle {
   public RexPermuteInputsShuttle(
       Mappings.TargetMapping mapping,
       RelNode... inputs) {
-    this(mapping, fields(inputs));
+    this(mapping, fields(inputs), false);
+  }
+
+  /**
+   * Creates a RexPermuteInputsShuttle with target type matching control.
+   *
+   * <p>The mapping provides at most one target for every source. If a source
path: core/src/main/java/org/apache/calcite/rex/RexPermuteInputsShuttle.java
user: login: xiedeyantu
body: Done!
created_at: 2025-05-01T00:35:59Z
author_association: MEMBER

PR comments:
user: login: xiedeyantu
created_at: 2025-04-30T04:31:23Z
body: This PR continues the unfinished work of #3452 .
author_association: MEMBER
user: login: mihaibudiu
created_at: 2025-05-01T00:37:48Z
body: I plan to merge this soon if there are no other comments.

author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-01T00:56:45Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=apache_calcite&pullRequest=4350) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=apache_calcite&pullRequest=4350&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=apache_calcite&pullRequest=4350&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=apache_calcite&pullRequest=4350&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=apache_calcite&pullRequest=4350&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=apache_calcite&pullRequest=4350&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=apache_calcite&pullRequest=4350)


author_association: NONE