This is an automated email from the ASF dual-hosted git repository.

asolimando 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 02bfbee0dd [CALCITE-7317] SubQueryRemoveRule should skip NULL-safety 
checks for IN subqueries when both the keys and the subquery columns are NOT 
NULL
02bfbee0dd is described below

commit 02bfbee0dd36f440aac3ec3a5250701250925e35
Author: Alessandro Solimando <[email protected]>
AuthorDate: Tue Dec 9 15:19:56 2025 +0100

    [CALCITE-7317] SubQueryRemoveRule should skip NULL-safety checks for IN 
subqueries when both the keys and the subquery columns are NOT NULL
---
 .../calcite/rel/rules/SubQueryRemoveRule.java      | 130 +++++++++--------
 .../org/apache/calcite/test/JdbcAdapterTest.java   |   4 +-
 .../org/apache/calcite/test/RelOptRulesTest.java   |  33 +++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 154 ++++++++++++++-------
 core/src/test/resources/sql/sub-query.iq           |  45 +++++-
 5 files changed, 260 insertions(+), 106 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java
index 6f13723170..8ed8c287b4 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java
@@ -583,11 +583,11 @@ private static RexNode rewriteIn(RexSubQuery e, 
Set<CorrelationId> variablesSet,
     //
     // select e.deptno,
     //   case
-    //   when ct.c = 0 then false
-    //   when e.deptno is null then null
-    //   when dt.i is not null then true
-    //   when ct.ck < ct.c then null
-    //   else false
+    //   when ct.c = 0 then false              -- (1) empty subquery check
+    //   when e.deptno is null then null       -- (2) key NULL check
+    //   when dt.i is not null then true       -- (3) match found
+    //   when ct.ck < ct.c then null           -- (4) NULLs exist in subquery
+    //   else false                             -- (5) no match
     //   end
     // from emp as e
     // left join (
@@ -595,37 +595,32 @@ private static RexNode rewriteIn(RexSubQuery e, 
Set<CorrelationId> variablesSet,
     //   cross join (select distinct deptno, true as i from emp)) as dt
     //   on e.deptno = dt.deptno
     //
-    // If keys are not null we can remove "ct" and simplify to
+    // If both keys (e.deptno) and subquery columns (deptno) are NOT NULL,
+    // we can drop checks (1), (2), and (4), which eliminates the need for ct:
     //
     // select e.deptno,
     //   case
-    //   when dt.i is not null then true
-    //   else false
+    //   when dt.i is not null then true       -- (3) match found
+    //   else false                             -- (5) no match
     //   end
     // from emp as e
     // left join (select distinct deptno, true as i from emp) as dt
     //   on e.deptno = dt.deptno
     //
-    // We could further simplify to
-    //
-    // select e.deptno,
-    //   dt.i is not null
-    // from emp as e
-    // left join (select distinct deptno, true as i from emp) as dt
-    //   on e.deptno = dt.deptno
+    // Check (1) is not needed: if the subquery is empty, all dt.i are NULL,
+    // and the LEFT JOIN pattern correctly returns FALSE for IN (TRUE for NOT 
IN).
     //
-    // but have not yet.
+    // NULL-safety checks are required if either the keys or the subquery
+    // columns are nullable, due to SQL three-valued logic.
     //
-    // If the logic is TRUE we can just kill the record if the condition
-    // evaluates to FALSE or UNKNOWN. Thus the query simplifies to an inner
-    // join:
+    // If the logic is TRUE (as opposed to TRUE_FALSE_UNKNOWN), we only care 
about
+    // matches, so the query simplifies to an inner join regardless of 
nullability:
     //
     // select e.deptno,
     //   true
     // from emp as e
     // inner join (select distinct deptno from emp) as dt
     //   on e.deptno = dt.deptno
-    //
 
     builder.push(e.rel);
     final List<RexNode> fields = new ArrayList<>(builder.fields());
@@ -671,6 +666,7 @@ private static RexNode rewriteIn(RexSubQuery e, 
Set<CorrelationId> variablesSet,
     final RexLiteral falseLiteral = builder.literal(false);
     final RexLiteral unknownLiteral =
         builder.getRexBuilder().makeNullLiteral(trueLiteral.getType());
+    boolean needsNullSafety = false;
     if (allLiterals) {
       final List<RexNode> conditions =
           Pair.zip(expressionOperands, fields).stream()
@@ -718,39 +714,51 @@ private static RexNode rewriteIn(RexSubQuery e, 
Set<CorrelationId> variablesSet,
       expressionOperands.clear();
       fields.clear();
     } else {
+      boolean anyFieldNullable = fields.stream()
+          .anyMatch(field -> field.getType().isNullable());
+
+      // we can skip NULL-safety checks only if both keys
+      // and subquery columns are NOT NULL
+      needsNullSafety =
+          (logic == RelOptUtil.Logic.TRUE_FALSE_UNKNOWN
+           || logic == RelOptUtil.Logic.UNKNOWN_AS_TRUE)
+          && (!keyIsNulls.isEmpty() || anyFieldNullable);
+
       switch (logic) {
       case TRUE:
         builder.aggregate(builder.groupKey(fields));
         break;
       case TRUE_FALSE_UNKNOWN:
       case UNKNOWN_AS_TRUE:
-        // Builds the cross join
-        // Some databases don't support use FILTER clauses for aggregate 
functions
-        // like {@code COUNT(*) FILTER (WHERE not(a is null))}
-        // So use count(*) when only one column
-        if (builder.fields().size() <= 1) {
-          builder.aggregate(builder.groupKey(),
-              builder.count(false, "c"),
-              builder.count(builder.fields()).as("ck"));
-        } else {
-          builder.aggregate(builder.groupKey(),
-              builder.count(false, "c"),
-              builder.count()
-                  .filter(builder
-                      .not(builder
-                          .and(builder.fields().stream()
-                              .map(builder::isNull)
-                              .collect(Collectors.toList()))))
-                  .as("ck"));
-        }
-        builder.as(ctAlias);
-        if (!variablesSet.isEmpty()) {
-          builder.join(JoinRelType.LEFT, trueLiteral, variablesSet);
-        } else {
-          builder.join(JoinRelType.INNER, trueLiteral, variablesSet);
+        if (needsNullSafety) {
+          // Builds the cross join
+          // Some databases don't support use FILTER clauses for aggregate 
functions
+          // like {@code COUNT(*) FILTER (WHERE not(a is null))}
+          // So use count(*) when only one column
+          if (builder.fields().size() <= 1) {
+            builder.aggregate(builder.groupKey(),
+                builder.count(false, "c"),
+                builder.count(builder.fields()).as("ck"));
+          } else {
+            builder.aggregate(builder.groupKey(),
+                builder.count(false, "c"),
+                builder.count()
+                    .filter(builder
+                        .not(builder
+                            .and(builder.fields().stream()
+                                .map(builder::isNull)
+                                .collect(Collectors.toList()))))
+                    .as("ck"));
+          }
+          builder.as(ctAlias);
+          if (!variablesSet.isEmpty()) {
+            builder.join(JoinRelType.LEFT, trueLiteral, variablesSet);
+          } else {
+            builder.join(JoinRelType.INNER, trueLiteral, variablesSet);
+          }
+          offset += 2;
+          builder.push(e.rel);
         }
-        offset += 2;
-        builder.push(e.rel);
         // fall through
       default:
         builder.aggregate(builder.groupKey(fields),
@@ -797,9 +805,12 @@ private static RexNode rewriteIn(RexSubQuery e, 
Set<CorrelationId> variablesSet,
             builder.equals(builder.field(dtAlias, "cs"), falseLiteral),
             b);
       } else {
-        operands.add(
-            builder.equals(builder.field(ctAlias, "c"), builder.literal(0)),
-            falseLiteral);
+        // only reference ctAlias if we created it
+        if (needsNullSafety) {
+          operands.add(
+              builder.equals(builder.field(ctAlias, "c"), builder.literal(0)),
+              falseLiteral);
+        }
       }
       break;
     default:
@@ -822,17 +833,28 @@ private static RexNode rewriteIn(RexSubQuery e, 
Set<CorrelationId> variablesSet,
       switch (logic) {
       case TRUE_FALSE_UNKNOWN:
       case UNKNOWN_AS_TRUE:
-        operands.add(
-            builder.lessThan(builder.field(ctAlias, "ck"),
-                builder.field(ctAlias, "c")),
-            b);
+        // only reference ctAlias if we created it
+        if (needsNullSafety) {
+          operands.add(
+              builder.lessThan(builder.field(ctAlias, "ck"),
+                  builder.field(ctAlias, "c")),
+              b);
+        }
         break;
       default:
         break;
       }
     }
     operands.add(falseLiteral);
-    return builder.call(SqlStdOperatorTable.CASE, operands.build());
+    RexNode result = builder.call(SqlStdOperatorTable.CASE, operands.build());
+
+    // When we skip NULL-safety checks, the result might be NOT NULL
+    // but the original IN expression was nullable, so we need to preserve that
+    if (e.getType().isNullable() && !result.getType().isNullable()) {
+      result = builder.getRexBuilder().makeCast(e.getType(), result, false, 
false);
+    }
+
+    return result;
   }
 
   /** Returns a reference to a particular field, by offset, across several
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 51e2aca144..a94cad903b 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -248,7 +248,7 @@ class JdbcAdapterTest {
         .query("select * from dept where deptno not in (select deptno from 
emp)")
         .explainContains("PLAN=JdbcToEnumerableConverter\n"
             + "  JdbcProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2])\n"
-            + "    JdbcFilter(condition=[OR(=($3, 0), AND(IS NULL($6), >=($4, 
$3)))])\n"
+            + "    JdbcFilter(condition=[OR(AND(IS NULL($6), >=($4, $3)), 
=($3, 0))])\n"
             + "      JdbcJoin(condition=[=($0, $5)], joinType=[left])\n"
             + "        JdbcJoin(condition=[true], joinType=[inner])\n"
             + "          JdbcTableScan(table=[[SCOTT, DEPT]])\n"
@@ -263,7 +263,7 @@ class JdbcAdapterTest {
             + "FROM \"SCOTT\".\"EMP\") AS \"t\"\n"
             + "LEFT JOIN (SELECT \"DEPTNO\", TRUE AS \"i\"\n"
             + "FROM \"SCOTT\".\"EMP\"\nGROUP BY \"DEPTNO\") AS \"t0\" ON 
\"DEPT\".\"DEPTNO\" = \"t0\".\"DEPTNO\"\n"
-            + "WHERE \"t\".\"c\" = 0 OR \"t0\".\"i\" IS NULL AND \"t\".\"ck\" 
>= \"t\".\"c\"");
+            + "WHERE \"t0\".\"i\" IS NULL AND \"t\".\"ck\" >= \"t\".\"c\" OR 
\"t\".\"c\" = 0");
   }
 
   @Test void testNotPushDownNotIn() {
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 ed3ffa32b0..33b72492f7 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -5621,6 +5621,39 @@ private void checkEmptyJoin(RelOptFixture f) {
     sql(sql).withSubQueryRules().check();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7317";>[CALCITE-7317]
+   * SubQueryRemoveRule should skip NULL-safety checks for IN subqueries when
+   * both the keys and the subquery columns are NOT NULL</a>. */
+  @Test void testInOptimizationBothNotNull() {
+    final String sql = "select * from emp as e1\n"
+        + "where empno in (\n"
+        + "  select empno from emp e2)";
+    sql(sql).withSubQueryRules().check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7317";>[CALCITE-7317]
+   * SubQueryRemoveRule should skip NULL-safety checks for IN subqueries when
+   * both the keys and the subquery columns are NOT NULL</a>. */
+  @Test void testNotInNullableSubqueryColumn() {
+    final String sql = "select * from empnullables as e1\n"
+        + "where coalesce(deptno, 0) not in (\n"
+        + "  select deptno from empnullables e2)";
+    sql(sql).withSubQueryRules().check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7317";>[CALCITE-7317]
+   * SubQueryRemoveRule should skip NULL-safety checks for IN subqueries when
+   * both the keys and the subquery columns are NOT NULL</a>. */
+  @Test void testNotInNullableKey() {
+    final String sql = "select * from empnullables as e1\n"
+        + "where deptno not in (\n"
+        + "  select coalesce(deptno, 0) from empnullables e2)";
+    sql(sql).withSubQueryRules().check();
+  }
+
   @Test void testSomeWithGreaterThanNoRowSubQuery() {
     final String sql = "select * from dept as d\n"
         + "where deptno > some(\n"
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 df13f4c78a..ab4ba1281b 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -6363,6 +6363,33 @@ LogicalProject(ENAME=[$26], NAME=[$16])
           LogicalJoin(condition=[=($9, $1)], joinType=[inner])
             LogicalTableScan(table=[[CATALOG, SALES, EMP]])
             LogicalTableScan(table=[[CATALOG, SALES, BONUS]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testInOptimizationBothNotNull">
+    <Resource name="sql">
+      <![CDATA[select * from emp as e1
+where empno in (
+  select empno from emp e2)]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[IN($0, {
+LogicalProject(EMPNO=[$0])
+  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(EMPNO=[$0])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>
@@ -8493,14 +8520,10 @@ LogicalProject(DEPTNO=[$0])
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(EMPNO=[$0])
-  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$13], NAME=[$14])
-    LogicalJoin(condition=[OR(=($9, 0), AND(IS NULL($12), >=($10, $9)))], 
joinType=[inner])
-      LogicalJoin(condition=[=($7, $11)], joinType=[left])
-        LogicalJoin(condition=[true], joinType=[inner])
-          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-          LogicalProject(c=[$0], ck=[$0])
-            LogicalAggregate(group=[{}], c=[COUNT()])
-              LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], DEPTNO0=[$11], NAME=[$12])
+    LogicalJoin(condition=[IS NULL($10)], joinType=[inner])
+      LogicalJoin(condition=[=($7, $9)], joinType=[left])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
         LogicalProject(DEPTNO=[$0], i=[true])
           LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
@@ -10705,6 +10728,72 @@ LogicalProject(USER=[USER])
 LogicalAggregate(group=[{0}], EXPR$1=[SUM($1)])
   LogicalProject(NAME=[$1], DEPTNO=[$0])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNotInNullableKey">
+    <Resource name="sql">
+      <![CDATA[select * from empnullables as e1
+where deptno not in (
+  select coalesce(deptno, 0) from empnullables e2)]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[NOT(IN($7, {
+LogicalProject(EXPR$0=[CASE(IS NOT NULL($7), CAST($7):INTEGER NOT NULL, 0)])
+  LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+}))])
+    LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[OR(=($9, 0), AND(IS NULL($12), >=($10, $9), IS 
NOT NULL($7)))])
+      LogicalJoin(condition=[=($7, $11)], joinType=[left])
+        LogicalJoin(condition=[true], joinType=[inner])
+          LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+          LogicalProject(c=[$0], ck=[$0])
+            LogicalAggregate(group=[{}], c=[COUNT()])
+              LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+        LogicalAggregate(group=[{0}], i=[LITERAL_AGG(true)])
+          LogicalProject(EXPR$0=[CASE(IS NOT NULL($7), CAST($7):INTEGER NOT 
NULL, 0)])
+            LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNotInNullableSubqueryColumn">
+    <Resource name="sql">
+      <![CDATA[select * from empnullables as e1
+where coalesce(deptno, 0) not in (
+  select deptno from empnullables e2)]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[NOT(IN(CASE(IS NOT NULL($7), CAST($7):INTEGER NOT 
NULL, 0), {
+LogicalProject(DEPTNO=[$7])
+  LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+}))])
+    LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+    LogicalFilter(condition=[NOT(AND(OR(IS NOT NULL($12), <($10, $9)), <>($9, 
0)))])
+      LogicalJoin(condition=[=(CASE(IS NOT NULL($7), CAST($7):INTEGER NOT 
NULL, 0), $11)], joinType=[left])
+        LogicalJoin(condition=[true], joinType=[inner])
+          LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+          LogicalAggregate(group=[{}], c=[COUNT()], ck=[COUNT($0)])
+            LogicalProject(DEPTNO=[$7])
+              LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+        LogicalAggregate(group=[{0}], i=[LITERAL_AGG(true)])
+          LogicalProject(DEPTNO=[$7])
+            LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
 ]]>
     </Resource>
   </TestCase>
@@ -20921,17 +21010,9 @@ LogicalProject(DEPTNO=[$0])
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(SAL=[$5])
-  LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($13), 
<($10, $9))))])
-    LogicalJoin(condition=[AND(=($0, $12), =($2, $14))], joinType=[left])
-      LogicalJoin(condition=[=($2, $11)], joinType=[left])
-        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-        LogicalProject(c=[CASE(IS NOT NULL($2), $2, 0)], ck=[CASE(IS NOT 
NULL($2), $2, 0)], NAME=[$0])
-          LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
-            LogicalAggregate(group=[{2}])
-              LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-            LogicalAggregate(group=[{0}], c=[COUNT()])
-              LogicalProject(NAME=[$1])
-                LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+  LogicalFilter(condition=[IS NULL($10)])
+    LogicalJoin(condition=[AND(=($0, $9), =($2, $11))], joinType=[left])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
       LogicalProject(DEPTNO=[$0], i=[true], NAME=[$1])
         LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
@@ -20940,14 +21021,9 @@ LogicalProject(SAL=[$5])
       <![CDATA[
 LogicalProject(SAL=[$5])
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-    LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($12), 
<($10, $9))))])
+    LogicalFilter(condition=[IS NULL($10)])
       LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{0, 2}])
-        LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{2}])
-          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-          LogicalProject(c=[$0], ck=[$0])
-            LogicalAggregate(group=[{}], c=[COUNT()])
-              LogicalFilter(condition=[=($cor0.JOB, $1)])
-                LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
         LogicalFilter(condition=[=($cor0.EMPNO, $0)])
           LogicalProject(DEPTNO=[$0], i=[true])
             LogicalFilter(condition=[=($cor0.JOB, $1)])
@@ -20978,15 +21054,9 @@ LogicalProject(EMPNO=[$1])
       <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-    LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($12), 
<($10, $9))))])
+    LogicalFilter(condition=[IS NULL($10)])
       LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{0, 1}])
-        LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{1}])
-          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-          LogicalProject(c=[$0], ck=[$0])
-            LogicalAggregate(group=[{}], c=[COUNT()])
-              LogicalFilter(condition=[AND(>($2, 2), =($cor0.ENAME, $0))])
-                LogicalProject(ENAME=[$1], EMPNO=[$0], R=[$5])
-                  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
         LogicalFilter(condition=[=($cor0.EMPNO, $0)])
           LogicalProject(EMPNO=[$1], i=[true])
             LogicalFilter(condition=[AND(>($2, 2), =($cor0.ENAME, $0))])
@@ -20997,19 +21067,9 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$
     <Resource name="planAfter">
       <![CDATA[
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-  LogicalFilter(condition=[OR(=($9, 0), IS NOT TRUE(OR(IS NOT NULL($13), 
<($10, $9))))])
-    LogicalJoin(condition=[AND(=($0, $12), =($1, $14))], joinType=[left])
-      LogicalJoin(condition=[=($1, $11)], joinType=[left])
-        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-        LogicalProject(c=[CASE(IS NOT NULL($2), $2, 0)], ck=[CASE(IS NOT 
NULL($2), $2, 0)], ENAME=[$0])
-          LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
-            LogicalAggregate(group=[{1}])
-              LogicalTableScan(table=[[CATALOG, SALES, EMP]])
-            LogicalAggregate(group=[{0}], c=[COUNT()])
-              LogicalProject(ENAME=[$0])
-                LogicalFilter(condition=[>($2, 2)])
-                  LogicalProject(ENAME=[$1], EMPNO=[$0], R=[$5])
-                    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+  LogicalFilter(condition=[IS NULL($10)])
+    LogicalJoin(condition=[AND(=($0, $9), =($1, $11))], joinType=[left])
+      LogicalTableScan(table=[[CATALOG, SALES, EMP]])
       LogicalProject(EMPNO=[$1], i=[true], ENAME=[$0])
         LogicalFilter(condition=[>($2, 2)])
           LogicalProject(ENAME=[$1], EMPNO=[$0], R=[$5])
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 2fa71122fb..59fda9fca2 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -2023,7 +2023,7 @@ select sal from "scott".emp
 (0 rows)
 
 !ok
-EnumerableCalc(expr#0..3=[{inputs}], expr#4=[IS NULL($t3)], expr#5=[NOT($t2)], 
expr#6=[IS NOT NULL($t2)], expr#7=[OR($t5, $t6)], expr#8=[IS NOT TRUE($t7)], 
expr#9=[OR($t4, $t8)], SAL=[$t1], $condition=[$t9])
+EnumerableCalc(expr#0..3=[{inputs}], expr#4=[NOT($t2)], expr#5=[IS NOT 
NULL($t2)], expr#6=[OR($t4, $t5)], expr#7=[IS NOT TRUE($t6)], expr#8=[IS 
NULL($t3)], expr#9=[OR($t7, $t8)], SAL=[$t1], $condition=[$t9])
   EnumerableNestedLoopJoin(condition=[true], joinType=[left])
     EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], SAL=[$t5])
       EnumerableTableScan(table=[[scott, EMP]])
@@ -2128,7 +2128,7 @@ select sal from "scott".emp
 (0 rows)
 
 !ok
-EnumerableCalc(expr#0..3=[{inputs}], expr#4=[IS NULL($t3)], expr#5=[NOT($t2)], 
expr#6=[IS NOT NULL($t2)], expr#7=[OR($t5, $t6)], expr#8=[IS NOT TRUE($t7)], 
expr#9=[OR($t4, $t8)], SAL=[$t1], $condition=[$t9])
+EnumerableCalc(expr#0..3=[{inputs}], expr#4=[NOT($t2)], expr#5=[IS NOT 
NULL($t2)], expr#6=[OR($t4, $t5)], expr#7=[IS NOT TRUE($t6)], expr#8=[IS 
NULL($t3)], expr#9=[OR($t7, $t8)], SAL=[$t1], $condition=[$t9])
   EnumerableNestedLoopJoin(condition=[true], joinType=[left])
     EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], SAL=[$t5])
       EnumerableTableScan(table=[[scott, EMP]])
@@ -2610,6 +2610,44 @@ EnumerableCalc(expr#0..1=[{inputs}], EXPR$0=[$t1])
 
 !use scott
 
+# [CALCITE-7317] SubQueryRemoveRule should skip NULL-safety checks for IN 
subqueries when both keys and subquery columns are NOT NULL
+select * from emp as e1 where empno in (select empno from emp e2);
+ EMPNO | ENAME  | JOB       | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO
+-------+--------+-----------+------+------------+---------+---------+--------
+  7369 | SMITH  | CLERK     | 7902 | 1980-12-17 |  800.00 |         |     20
+  7499 | ALLEN  | SALESMAN  | 7698 | 1981-02-20 | 1600.00 |  300.00 |     30
+  7521 | WARD   | SALESMAN  | 7698 | 1981-02-22 | 1250.00 |  500.00 |     30
+  7566 | JONES  | MANAGER   | 7839 | 1981-02-04 | 2975.00 |         |     20
+  7654 | MARTIN | SALESMAN  | 7698 | 1981-09-28 | 1250.00 | 1400.00 |     30
+  7698 | BLAKE  | MANAGER   | 7839 | 1981-01-05 | 2850.00 |         |     30
+  7782 | CLARK  | MANAGER   | 7839 | 1981-06-09 | 2450.00 |         |     10
+  7788 | SCOTT  | ANALYST   | 7566 | 1987-04-19 | 3000.00 |         |     20
+  7839 | KING   | PRESIDENT |      | 1981-11-17 | 5000.00 |         |     10
+  7844 | TURNER | SALESMAN  | 7698 | 1981-09-08 | 1500.00 |    0.00 |     30
+  7876 | ADAMS  | CLERK     | 7788 | 1987-05-23 | 1100.00 |         |     20
+  7900 | JAMES  | CLERK     | 7698 | 1981-12-03 |  950.00 |         |     30
+  7902 | FORD   | ANALYST   | 7566 | 1981-12-03 | 3000.00 |         |     20
+  7934 | MILLER | CLERK     | 7782 | 1982-01-23 | 1300.00 |         |     10
+(14 rows)
+
+!ok
+
+# [CALCITE-7317] SubQueryRemoveRule should skip NULL-safety checks for IN 
subqueries when both keys and subquery columns are NOT NULL
+select * from emp as e1 where coalesce(deptno, 0) not in (select deptno from 
emp e2);
+ EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO
+-------+-------+-----+-----+----------+-----+------+--------
+(0 rows)
+
+!ok
+
+# [CALCITE-7317] SubQueryRemoveRule should skip NULL-safety checks for IN 
subqueries when both keys and subquery columns are NOT NULL
+select * from emp as e1 where deptno not in (select coalesce(deptno, 0) from 
emp e2);
+ EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO
+-------+-------+-----+-----+----------+-----+------+--------
+(0 rows)
+
+!ok
+
 # [CALCITE-1513] Correlated NOT IN query throws AssertionError
 select count(*) as c
 from "scott".emp as e
@@ -3920,7 +3958,7 @@ select * from "scott".emp where empno not in (null, 7782);
 
 !ok
 
-EnumerableCalc(expr#0..12=[{inputs}], expr#13=[0], expr#14=[=($t8, $t13)], 
expr#15=[IS NULL($t12)], expr#16=[>=($t9, $t8)], expr#17=[AND($t15, $t16)], 
expr#18=[OR($t14, $t17)], proj#0..7=[{exprs}], $condition=[$t18])
+EnumerableCalc(expr#0..12=[{inputs}], expr#13=[IS NULL($t12)], 
expr#14=[>=($t9, $t8)], expr#15=[AND($t13, $t14)], expr#16=[0], expr#17=[=($t8, 
$t16)], expr#18=[OR($t15, $t17)], proj#0..7=[{exprs}], $condition=[$t18])
   EnumerableMergeJoin(condition=[=($10, $11)], joinType=[left])
     EnumerableSort(sort0=[$10], dir0=[ASC])
       EnumerableCalc(expr#0..9=[{inputs}], expr#10=[CAST($t0):INTEGER NOT 
NULL], proj#0..10=[{exprs}])
@@ -5505,4 +5543,5 @@ WHERE EXISTS
 (2 rows)
 
 !ok
+
 # End sub-query.iq

Reply via email to