Skip to content

Commit 103d5f9

Browse files
[CALCITE-5390] RelDecorrelator throws NullPointerException
1 parent 3b5fcf5 commit 103d5f9

5 files changed

Lines changed: 418 additions & 7 deletions

File tree

core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,7 @@ protected RexNode removeCorrelationExpr(
918918
* CASE WHEN cnt0 IS NOT NULL THEN cnt0 ELSE 0 END AS cnt
919919
* FROM (SELECT deptno FROM dept GROUP BY deptno) d2
920920
* LEFT JOIN (
921-
* SELECT deptno, COUNT(e.empno) cnt0
921+
* SELECT deptno, COUNT(emp.empno) cnt0
922922
* FROM emp
923923
* WHERE deptno IS NOT NULL
924924
* GROUP BY deptno) e
@@ -1421,7 +1421,8 @@ private static void shiftMapping(Map<Integer, Integer> mapping, int startIndex,
14211421
for (CorRef corVar : correlations) {
14221422
final int oldCorVarOffset = corVar.field;
14231423

1424-
final RelNode oldInput = requireNonNull(getCorRel(corVar));
1424+
final RelNode oldInput = findInputRel(corVar);
1425+
14251426
final Frame frame = requireNonNull(getOrCreateFrame(oldInput));
14261427
final RelNode newInput = frame.r;
14271428

@@ -1453,7 +1454,7 @@ private static void shiftMapping(Map<Integer, Integer> mapping, int startIndex,
14531454

14541455
RelNode r = null;
14551456
for (CorRef corVar : correlations) {
1456-
final RelNode oldInput = requireNonNull(getCorRel(corVar));
1457+
final RelNode oldInput = findInputRel(corVar);
14571458
final RelNode newInput = requireNonNull(getOrCreateFrame(oldInput).r);
14581459

14591460
if (!joinedInputs.contains(newInput)) {
@@ -1487,7 +1488,7 @@ private static void shiftMapping(Map<Integer, Integer> mapping, int startIndex,
14871488
for (CorRef corRef : correlations) {
14881489
// The first input of a Correlate is always the rel defining
14891490
// the correlated variables.
1490-
final RelNode oldInput = requireNonNull(getCorRel(corRef));
1491+
final RelNode oldInput = findInputRel(corRef);
14911492
final Frame frame = getOrCreateFrame(oldInput);
14921493
final RelNode newInput = requireNonNull(frame.r);
14931494

@@ -1533,6 +1534,39 @@ private RelNode getCorRel(CorRef corVar) {
15331534
() -> "r.getInput(0) is null for " + r);
15341535
}
15351536

1537+
/**
1538+
* Finds the RelNode that produces the given correlation variable.
1539+
*
1540+
* <p>This method resolves correlation variables by inspecting the {@link #frameStack},
1541+
* which maintains the active correlation contexts during the top-down traversal.
1542+
*
1543+
* <p>The lookup logic implements <b>Lexical Scoping</b> (with Shadowing):
1544+
* <ul>
1545+
* <li>The {@code frameStack} is traversed from top to bottom (most recently pushed to
1546+
* least recently pushed). This ensures that if multiple nested queries use the same
1547+
* {@link CorrelationId}, the innermost definition takes precedence, shadowing outer ones.
1548+
* </li>
1549+
* </ul>
1550+
*
1551+
* <p>If the variable is not found in the {@code frameStack} (e.g., it might be defined outside
1552+
* the current traversal path or in a global context), the method falls back to looking it up
1553+
* in the global {@link #cm} (CorelMap).
1554+
*
1555+
* @param corVar The correlation variable reference to resolve.
1556+
* @return The {@link RelNode} that produces the correlation variable.
1557+
*/
1558+
private RelNode findInputRel(CorRef corVar) {
1559+
final int oldCorVarOffset = corVar.field;
1560+
for (Pair<CorrelationId, Frame> pair : frameStack) {
1561+
if (pair.left.equals(corVar.corr)) {
1562+
if (oldCorVarOffset < pair.right.oldRel.getRowType().getFieldCount()) {
1563+
return pair.right.oldRel;
1564+
}
1565+
}
1566+
}
1567+
return getCorRel(corVar);
1568+
}
1569+
15361570
/** Adds a value generator to satisfy the correlating variables used by
15371571
* a relational expression, if those variables are not already provided by
15381572
* its input. */
@@ -3766,12 +3800,16 @@ private RexVisitorImpl<Void> rexVisitor(final RelNode rel) {
37663800
* and where to find the output fields and correlation variables
37673801
* among its output fields. */
37683802
static class Frame {
3803+
// The original relational expression before decorrelation
3804+
final RelNode oldRel;
3805+
// The decorrelated relational expression
37693806
final RelNode r;
37703807
final ImmutableSortedMap<CorDef, Integer> corDefOutputs;
37713808
final ImmutableSortedMap<Integer, Integer> oldToNewOutputs;
37723809

37733810
Frame(RelNode oldRel, RelNode r, NavigableMap<CorDef, Integer> corDefOutputs,
37743811
Map<Integer, Integer> oldToNewOutputs) {
3812+
this.oldRel = requireNonNull(oldRel, "oldRel");
37753813
this.r = requireNonNull(r, "r");
37763814
this.corDefOutputs = ImmutableSortedMap.copyOf(corDefOutputs);
37773815
this.oldToNewOutputs = ImmutableSortedMap.copyOf(oldToNewOutputs);

core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -965,8 +965,6 @@ public TopDownGeneralDecorrelator getVisitor() {
965965
* Unnesting information.
966966
*/
967967
static class UnnestedQuery extends Frame {
968-
final RelNode oldRel;
969-
970968
/**
971969
* Creates a UnnestedQuery.
972970
*
@@ -978,7 +976,6 @@ static class UnnestedQuery extends Frame {
978976
UnnestedQuery(RelNode oldRel, RelNode r, NavigableMap<CorDef, Integer> corDefOutputs,
979977
Map<Integer, Integer> oldToNewOutputs) {
980978
super(oldRel, r, corDefOutputs, oldToNewOutputs);
981-
this.oldRel = oldRel;
982979
}
983980

984981
/**

core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,95 @@ public static Frameworks.ConfigBuilder config() {
15711571
assertThat(after, hasTree(planAfter));
15721572
}
15731573

1574+
/** Test case for <a href="https://issues.apache.org/jira/browse/CALCITE-5390">[CALCITE-5390]
1575+
* RelDecorrelator throws NullPointerException</a>. */
1576+
@Test void testCorrelationLexicalScoping() {
1577+
final FrameworkConfig frameworkConfig = config().build();
1578+
final RelBuilder builder = RelBuilder.create(frameworkConfig);
1579+
final RelOptCluster cluster = builder.getCluster();
1580+
final Planner planner = Frameworks.getPlanner(frameworkConfig);
1581+
final String sql = ""
1582+
+ "select deptno,\n"
1583+
+ " (select min(1) from emp where empno > d.deptno) as i0,\n"
1584+
+ " (select min(0) from emp where deptno = d.deptno and "
1585+
+ "ename = 'SMITH' and d.deptno > 0) as i1\n"
1586+
+ "from dept as d";
1587+
final RelNode originalRel;
1588+
try {
1589+
final SqlNode parse = planner.parse(sql);
1590+
final SqlNode validate = planner.validate(parse);
1591+
originalRel = planner.rel(validate).rel;
1592+
} catch (Exception e) {
1593+
throw TestUtil.rethrow(e);
1594+
}
1595+
1596+
final HepProgram hepProgram = HepProgram.builder()
1597+
.addRuleCollection(
1598+
ImmutableList.of(
1599+
// SubQuery program rules
1600+
CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
1601+
CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
1602+
CoreRules.JOIN_SUB_QUERY_TO_CORRELATE))
1603+
.build();
1604+
final Program program =
1605+
Programs.of(hepProgram, true,
1606+
requireNonNull(cluster.getMetadataProvider()));
1607+
final RelNode before =
1608+
program.run(cluster.getPlanner(), originalRel, cluster.traitSet(),
1609+
Collections.emptyList(), Collections.emptyList());
1610+
final String planBefore = ""
1611+
+ "LogicalProject(DEPTNO=[$0], I0=[$3], I1=[$4])\n"
1612+
+ " LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}])\n"
1613+
+ " LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}])\n"
1614+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1615+
+ " LogicalAggregate(group=[{}], EXPR$0=[MIN($0)])\n"
1616+
+ " LogicalProject($f0=[1])\n"
1617+
+ " LogicalFilter(condition=[>($0, CAST($cor0.DEPTNO):SMALLINT NOT NULL)])\n"
1618+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1619+
+ " LogicalAggregate(group=[{}], EXPR$0=[MIN($0)])\n"
1620+
+ " LogicalProject($f0=[0])\n"
1621+
+ " LogicalFilter(condition=[AND(=($7, $cor0.DEPTNO), =($1, 'SMITH'), >(CAST($cor0.DEPTNO):INTEGER NOT NULL, 0))])\n"
1622+
+ " LogicalTableScan(table=[[scott, EMP]])\n";
1623+
assertThat(before, hasTree(planBefore));
1624+
1625+
// Decorrelate without any rules, just "purely" decorrelation algorithm on RelDecorrelator
1626+
final RelNode after =
1627+
RelDecorrelator.decorrelateQuery(before, builder, RuleSets.ofList(Collections.emptyList()),
1628+
RuleSets.ofList(Collections.emptyList()));
1629+
final String planAfter = ""
1630+
+ "LogicalProject(DEPTNO=[$0], I0=[$3], I1=[$8])\n"
1631+
+ " LogicalJoin(condition=[AND(=($0, $6), =($5, $7))], joinType=[left])\n"
1632+
+ " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EXPR$0=[$5], DEPTNO0=[$0], $f5=[>(CAST($0):INTEGER NOT NULL, 0)])\n"
1633+
+ " LogicalJoin(condition=[=($3, $4)], joinType=[left])\n"
1634+
+ " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1635+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1636+
+ " LogicalAggregate(group=[{0}], EXPR$0=[MIN($1)])\n"
1637+
+ " LogicalProject(DEPTNO0=[$8], $f0=[1])\n"
1638+
+ " LogicalJoin(condition=[>($0, $8)], joinType=[inner])\n"
1639+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1640+
+ " LogicalAggregate(group=[{0}])\n"
1641+
+ " LogicalProject(DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1642+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1643+
+ " LogicalAggregate(group=[{0, 1}], EXPR$0=[MIN($2)])\n"
1644+
+ " LogicalProject(DEPTNO0=[$8], $f5=[$9], $f0=[0])\n"
1645+
+ " LogicalJoin(condition=[=($7, $8)], joinType=[inner])\n"
1646+
+ " LogicalFilter(condition=[=($1, 'SMITH')])\n"
1647+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1648+
+ " LogicalFilter(condition=[$1])\n"
1649+
+ " LogicalProject(DEPTNO=[$0], $f5=[>(CAST($0):INTEGER NOT NULL, 0)])\n"
1650+
+ " LogicalJoin(condition=[=($3, $4)], joinType=[left])\n"
1651+
+ " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1652+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1653+
+ " LogicalAggregate(group=[{0}], EXPR$0=[MIN($1)])\n"
1654+
+ " LogicalProject(DEPTNO0=[$8], $f0=[1])\n"
1655+
+ " LogicalJoin(condition=[>($0, $8)], joinType=[inner])\n"
1656+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1657+
+ " LogicalAggregate(group=[{0}])\n"
1658+
+ " LogicalProject(DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1659+
+ " LogicalTableScan(table=[[scott, DEPT]])\n";
1660+
assertThat(after, hasTree(planAfter));
1661+
}
1662+
15741663
/** Test case for <a href="https://issues.apache.org/jira/browse/CALCITE-7320">[CALCITE-7320]
15751664
* AggregateProjectMergeRule throws AssertionError when Project maps multiple grouping keys
15761665
* to the same field</a>. */

core/src/test/resources/sql/sub-query.iq

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8729,4 +8729,79 @@ where e.deptno = 10;
87298729

87308730
!ok
87318731

8732+
# [CALCITE-5390] RelDecorrelator throws NullPointerException
8733+
# Verified against PostgreSQL.
8734+
select deptno,
8735+
(select min(1) from emp where empno > d.deptno) as i0,
8736+
(select min(0) from emp where deptno = d.deptno and ename = 'SMITH' and d.deptno > 0) as i1
8737+
from dept as d;
8738+
+--------+----+----+
8739+
| DEPTNO | I0 | I1 |
8740+
+--------+----+----+
8741+
| 10 | 1 | |
8742+
| 20 | 1 | 0 |
8743+
| 30 | 1 | |
8744+
| 40 | 1 | |
8745+
+--------+----+----+
8746+
(4 rows)
8747+
8748+
!ok
8749+
8750+
# [CALCITE-5390] RelDecorrelator throws NullPointerException
8751+
# Verified against PostgreSQL.
8752+
SELECT
8753+
(SELECT 1 FROM emp d WHERE d.job = a.job LIMIT 1) AS t1,
8754+
(SELECT a.job = 'PRESIDENT' FROM emp s LIMIT 1) as t2
8755+
FROM emp a;
8756+
+----+-------+
8757+
| T1 | T2 |
8758+
+----+-------+
8759+
| 1 | false |
8760+
| 1 | false |
8761+
| 1 | false |
8762+
| 1 | false |
8763+
| 1 | false |
8764+
| 1 | false |
8765+
| 1 | false |
8766+
| 1 | false |
8767+
| 1 | false |
8768+
| 1 | false |
8769+
| 1 | false |
8770+
| 1 | false |
8771+
| 1 | false |
8772+
| 1 | true |
8773+
+----+-------+
8774+
(14 rows)
8775+
8776+
!ok
8777+
8778+
# [CALCITE-5390] RelDecorrelator throws NullPointerException
8779+
# Verified against PostgreSQL.
8780+
SELECT *
8781+
FROM emp e
8782+
WHERE e.ename NOT IN (
8783+
SELECT d.dname
8784+
FROM dept d
8785+
WHERE e.deptno = d.deptno OR e.sal > 2000.0);
8786+
+-------+--------+-----------+------+------------+---------+---------+--------+
8787+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
8788+
+-------+--------+-----------+------+------------+---------+---------+--------+
8789+
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | | 20 |
8790+
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
8791+
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
8792+
| 7566 | JONES | MANAGER | 7839 | 1981-02-04 | 2975.00 | | 20 |
8793+
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
8794+
| 7698 | BLAKE | MANAGER | 7839 | 1981-01-05 | 2850.00 | | 30 |
8795+
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | | 10 |
8796+
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | | 20 |
8797+
| 7839 | KING | PRESIDENT | | 1981-11-17 | 5000.00 | | 10 |
8798+
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
8799+
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | | 20 |
8800+
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | | 30 |
8801+
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | | 20 |
8802+
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | | 10 |
8803+
+-------+--------+-----------+------+------------+---------+---------+--------+
8804+
(14 rows)
8805+
8806+
!ok
87328807
# End sub-query.iq

0 commit comments

Comments
 (0)