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 24f0278e96 [CALCITE-7821] Correlated query crashes SqlToRelConverter
24f0278e96 is described below

commit 24f0278e969d1fca465d1a3cbed87ef09f9522fb
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu Sep 24 11:50:26 2026 -0700

    [CALCITE-7821] Correlated query crashes SqlToRelConverter
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../apache/calcite/sql2rel/SqlToRelConverter.java  | 33 ++++++---
 .../apache/calcite/test/SqlToRelConverterTest.java | 25 +++++++
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 83 ++++++++++++++++++++++
 core/src/test/resources/sql/sub-query.iq           | 66 +++++++++++++++++
 4 files changed, 197 insertions(+), 10 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java 
b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 0223c778a6..ce6c00e295 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -30,6 +30,7 @@
 import org.apache.calcite.plan.ViewExpanders;
 import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.prepare.RelOptTableImpl;
+import org.apache.calcite.rel.BiRel;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelCollations;
@@ -47,7 +48,6 @@
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.Join;
-import org.apache.calcite.rel.core.JoinInfo;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.core.Project;
 import org.apache.calcite.rel.core.RelFactories;
@@ -1514,9 +1514,17 @@ private void substituteSubQuery(Blackboard bb, SubQuery 
subQuery) {
                     AggregateCall.create(call.getParserPosition(), 
SqlStdOperatorTable.COUNT, false,
                         false, false, ImmutableList.of(), args,
                         -1, null, RelCollations.EMPTY, longType, null)));
-        LogicalJoin join =
-            LogicalJoin.create(bb.root(), aggregate, ImmutableList.of(),
-                rexBuilder.makeLiteral(true), ImmutableSet.of(), 
JoinRelType.INNER);
+        // seek may reference the outer query; createJoin then returns
+        // a Correlate that binds the variable.
+        final RelNode join =
+            createJoin(bb, bb.root(), aggregate, rexBuilder.makeLiteral(true),
+                JoinRelType.INNER);
+        // As in Blackboard.register, keep the group by mapping for the new 
root
+        final Map<Integer, Integer> projection =
+            bb.mapRootRelToFieldProjection.get(bb.root());
+        if (projection != null) {
+          bb.mapRootRelToFieldProjection.put(join, projection);
+        }
         bb.setRoot(join, false);
       }
       final RexNode rex =
@@ -1811,9 +1819,10 @@ private RexNode translateIn(RelOptUtil.Logic logic, 
@Nullable RelNode root,
       // cross join (select count(*) as c, count(deptno) as ck from v) as ct
       // left join (select distinct deptno, true as i from v) as dt
       //   on e.deptno = dt.deptno
-      final Join join = (Join) requireNonNull(root, "root");
+      // Either join is a Correlate when the sub-query is correlated.
+      final BiRel join = (BiRel) requireNonNull(root, "root");
       final Project left = (Project) join.getLeft();
-      final RelNode leftLeft = ((Join) left.getInput()).getLeft();
+      final RelNode leftLeft = ((BiRel) left.getInput()).getLeft();
       final int leftLeftCount = leftLeft.getRowType().getFieldCount();
       final RelDataType longType =
           typeFactory.createSqlType(SqlTypeName.BIGINT);
@@ -1834,10 +1843,14 @@ private RexNode translateIn(RelOptUtil.Logic logic, 
@Nullable RelNode root,
           falseLiteral,
           rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, iRef),
           trueLiteral);
-      final JoinInfo joinInfo = join.analyzeCondition();
-      for (int leftKey : joinInfo.leftKeys) {
-        final RexNode kRef = rexBuilder.makeInputRef(root, leftKey);
-        args.add(rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, kRef),
+      // Take the IN keys from rex rather than from the join condition,
+      // because a Correlate has no condition. rex has the layout described
+      // in TRUE_FALSE.
+      final int keyCount = (rex.getType().getFieldCount() - 1) / 2;
+      for (int i = 0; i < keyCount; i++) {
+        args.add(
+            rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL,
+                rexBuilder.makeFieldAccess(rex, i)),
             unknownLiteral);
       }
       args.add(rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN, ckRef, cRef),
diff --git 
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 4b26c3f284..dbce3a21c0 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -2728,6 +2728,31 @@ void checkCorrelatedMapSubQuery(boolean expand) {
     sql(sql).ok();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7821";>[CALCITE-7821]
+   * Correlated query crashes SqlToRelConverter</a>. */
+  @Test void testNotInCorrelatedNullable() {
+    final String sql = "select * from emp as t where t.mgr not in"
+        + " (select s.mgr from emp as s where s.empno = t.empno)";
+    sql(sql).withExpand(true).ok();
+  }
+
+  /** As {@link #testNotInCorrelatedNullable()}, but without decorrelation. */
+  @Test void testNotInCorrelatedNullableNoDecorrelate() {
+    final String sql = "select * from emp as t where t.mgr not in"
+        + " (select s.mgr from emp as s where s.empno = t.empno)";
+    sql(sql).withExpand(true).withDecorrelate(false).ok();
+  }
+
+  /** As {@link #testNotInCorrelatedNullable()}, but in HAVING, where the
+   * outer query is an aggregate. */
+  @Test void testNotInCorrelatedNullableHaving() {
+    final String sql = "select deptno from emp as e group by deptno\n"
+        + "having deptno not in\n"
+        + " (select s.mgr from emp as s where s.deptno = e.deptno)";
+    sql(sql).withExpand(true).ok();
+  }
+
   @Test void testAllValueList() {
     final String sql = "select empno from emp where deptno > all (10, 20)";
     sql(sql).withExpand(false).ok();
diff --git 
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml 
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 21db30b73e..71376384ef 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -6509,6 +6509,89 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$
         LogicalProject($f0=[true])
           LogicalFilter(condition=[=($cor0.DEPTNO, $0)])
             LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNotInCorrelatedNullable">
+    <Resource name="sql">
+      <![CDATA[select * from emp as t where t.mgr not in (select s.mgr from 
emp as s where s.empno = t.empno)]]>
+    </Resource>
+    <Resource name="plan">
+      <![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), AND(IS NULL($14), >=($10, $9), IS NOT 
NULL($11)))])
+    LogicalJoin(condition=[AND(=($0, $13), =($11, $12))], joinType=[left])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f0=[$10], 
$f1=[$11], MGR0=[$3])
+        LogicalJoin(condition=[=($0, $9)], joinType=[inner])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+          LogicalProject(EMPNO=[$0], $f1=[CASE(IS NOT NULL($2), $2, 0)], 
$f2=[CASE(IS NOT NULL($3), $3, 0)])
+            LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+              LogicalAggregate(group=[{0}])
+                LogicalProject(EMPNO=[$0])
+                  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+              LogicalAggregate(group=[{0}], agg#0=[COUNT()], agg#1=[COUNT($1)])
+                LogicalProject(EMPNO=[$0], MGR=[$3], $f1=[true])
+                  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalFilter(condition=[IS NOT NULL($0)])
+        LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
+          LogicalProject(MGR=[$3], EMPNO=[$0], $f1=[true])
+            LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNotInCorrelatedNullableHaving">
+    <Resource name="sql">
+      <![CDATA[select deptno from emp as e group by deptno
+having deptno not in
+ (select s.mgr from emp as s where s.deptno = e.deptno)]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(DEPTNO=[$0])
+  LogicalFilter(condition=[OR(=($1, 0), AND(IS NULL($6), >=($2, $1)))])
+    LogicalJoin(condition=[AND(=($0, $5), =($3, $4))], joinType=[left])
+      LogicalProject(DEPTNO=[$0], $f0=[$2], $f1=[$3], DEPTNO0=[$0])
+        LogicalJoin(condition=[=($0, $1)], joinType=[inner])
+          LogicalAggregate(group=[{0}])
+            LogicalProject(DEPTNO=[$7])
+              LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+          LogicalProject(DEPTNO=[$0], $f1=[CASE(IS NOT NULL($2), $2, 0)], 
$f2=[CASE(IS NOT NULL($3), $3, 0)])
+            LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+              LogicalAggregate(group=[{0}])
+                LogicalAggregate(group=[{0}])
+                  LogicalProject(DEPTNO=[$7])
+                    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+              LogicalAggregate(group=[{0}], agg#0=[COUNT()], agg#1=[COUNT($1)])
+                LogicalProject(DEPTNO=[$7], MGR=[$3], $f1=[true])
+                  LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalFilter(condition=[IS NOT NULL($0)])
+        LogicalAggregate(group=[{0, 1}], agg#0=[MIN($2)])
+          LogicalProject(MGR=[$3], DEPTNO=[$7], $f1=[true])
+            LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNotInCorrelatedNullableNoDecorrelate">
+    <Resource name="sql">
+      <![CDATA[select * from emp as t where t.mgr not in (select s.mgr from 
emp as s where s.empno = t.empno)]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[NOT(CASE(=($9, 0), false, IS NOT NULL($13), true, 
IS NULL($11), null:BOOLEAN, <($10, $9), null:BOOLEAN, false))])
+    LogicalCorrelate(correlation=[$cor0], joinType=[left], 
requiredColumns=[{0, 11}])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8], $f0=[$9], 
$f1=[$10], MGR0=[$3])
+        LogicalCorrelate(correlation=[$cor0], joinType=[inner], 
requiredColumns=[{0}])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+          LogicalAggregate(group=[{}], agg#0=[COUNT()], agg#1=[COUNT($0)])
+            LogicalProject(MGR=[$3], $f1=[true])
+              LogicalFilter(condition=[=($0, $cor0.EMPNO)])
+                LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalFilter(condition=[=($cor0.MGR0, $0)])
+        LogicalAggregate(group=[{0}], agg#0=[MIN($1)])
+          LogicalProject(MGR=[$3], $f1=[true])
+            LogicalFilter(condition=[=($0, $cor0.EMPNO)])
+              LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 4a71d6a388..717697d29f 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -10231,3 +10231,69 @@ where e.n > (
 
 !ok
 !set trimfields true
+
+# [CALCITE-7821] Correlated query crashes SqlToRelConverter
+# The results of the following four queries were validated using Postgres.
+!set expand true
+with
+t(k, a) as (select * from (values (1, 10), (2, cast(null as int)), (3, 30), 
(4, 40), (5, cast(null as int)))),
+s(k, b) as (select * from (values (1, 10), (3, cast(null as int)), (4, 41), 
(5, 50)))
+select * from t where t.a not in (select s.b from s where s.k = t.k);
++---+----+
+| K | A  |
++---+----+
+| 2 |    |
+| 4 | 40 |
++---+----+
+(2 rows)
+
+!ok
+
+with
+t(k, a) as (select * from (values (1, 10), (2, cast(null as int)), (3, 30), 
(4, 40), (5, cast(null as int)))),
+s(k, b) as (select * from (values (1, 10), (3, cast(null as int)), (4, 41), 
(5, 50)))
+select k, a in (select s.b from s where s.k = t.k) as i from t;
++---+-------+
+| K | I     |
++---+-------+
+| 1 | true  |
+| 2 | false |
+| 3 |       |
+| 4 | false |
+| 5 |       |
++---+-------+
+(5 rows)
+
+!ok
+
+with
+t(k, a) as (select * from (values (1, 10), (2, cast(null as int)), (3, 30), 
(4, 40), (5, cast(null as int)))),
+s(k, b) as (select * from (values (1, 10), (3, cast(null as int)), (4, 41), 
(5, 50)))
+select k, a not in (select s.b from s where s.k = t.k) as ni from t;
++---+-------+
+| K | NI    |
++---+-------+
+| 1 | false |
+| 2 | true  |
+| 3 |       |
+| 4 | true  |
+| 5 |       |
++---+-------+
+(5 rows)
+
+!ok
+
+with
+t(a, k) as (select * from (values (10, 1), (cast(null as int), 2), (30, 3), 
(40, 4), (cast(null as int), 5))),
+s(k, b) as (select * from (values (1, 10), (3, cast(null as int)), (4, 41), 
(5, 50)))
+select k from t group by k having max(a) not in (select s.b from s where s.k = 
t.k);
++---+
+| K |
++---+
+| 2 |
+| 4 |
++---+
+(2 rows)
+
+!ok
+!set expand false

Reply via email to