Skip to content

Commit 69a7a06

Browse files
[CALCITE-5390] RelDecorrelator throws NullPointerException
1 parent f181e07 commit 69a7a06

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
@@ -973,8 +973,6 @@ public TopDownGeneralDecorrelator getVisitor() {
973973
* Unnesting information.
974974
*/
975975
static class UnnestedQuery extends Frame {
976-
final RelNode oldRel;
977-
978976
/**
979977
* Creates a UnnestedQuery.
980978
*
@@ -986,7 +984,6 @@ static class UnnestedQuery extends Frame {
986984
UnnestedQuery(RelNode oldRel, RelNode r, NavigableMap<CorDef, Integer> corDefOutputs,
987985
Map<Integer, Integer> oldToNewOutputs) {
988986
super(oldRel, r, corDefOutputs, oldToNewOutputs);
989-
this.oldRel = oldRel;
990987
}
991988

992989
/**

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

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

1672+
/** Test case for <a href="https://issues.apache.org/jira/browse/CALCITE-5390">[CALCITE-5390]
1673+
* RelDecorrelator throws NullPointerException</a>. */
1674+
@Test void testCorrelationLexicalScoping() {
1675+
final FrameworkConfig frameworkConfig = config().build();
1676+
final RelBuilder builder = RelBuilder.create(frameworkConfig);
1677+
final RelOptCluster cluster = builder.getCluster();
1678+
final Planner planner = Frameworks.getPlanner(frameworkConfig);
1679+
final String sql = ""
1680+
+ "select deptno,\n"
1681+
+ " (select min(1) from emp where empno > d.deptno) as i0,\n"
1682+
+ " (select min(0) from emp where deptno = d.deptno and "
1683+
+ "ename = 'SMITH' and d.deptno > 0) as i1\n"
1684+
+ "from dept as d";
1685+
final RelNode originalRel;
1686+
try {
1687+
final SqlNode parse = planner.parse(sql);
1688+
final SqlNode validate = planner.validate(parse);
1689+
originalRel = planner.rel(validate).rel;
1690+
} catch (Exception e) {
1691+
throw TestUtil.rethrow(e);
1692+
}
1693+
1694+
final HepProgram hepProgram = HepProgram.builder()
1695+
.addRuleCollection(
1696+
ImmutableList.of(
1697+
// SubQuery program rules
1698+
CoreRules.FILTER_SUB_QUERY_TO_CORRELATE,
1699+
CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE,
1700+
CoreRules.JOIN_SUB_QUERY_TO_CORRELATE))
1701+
.build();
1702+
final Program program =
1703+
Programs.of(hepProgram, true,
1704+
requireNonNull(cluster.getMetadataProvider()));
1705+
final RelNode before =
1706+
program.run(cluster.getPlanner(), originalRel, cluster.traitSet(),
1707+
Collections.emptyList(), Collections.emptyList());
1708+
final String planBefore = ""
1709+
+ "LogicalProject(DEPTNO=[$0], I0=[$3], I1=[$4])\n"
1710+
+ " LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}])\n"
1711+
+ " LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}])\n"
1712+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1713+
+ " LogicalAggregate(group=[{}], EXPR$0=[MIN($0)])\n"
1714+
+ " LogicalProject($f0=[1])\n"
1715+
+ " LogicalFilter(condition=[>($0, CAST($cor0.DEPTNO):SMALLINT NOT NULL)])\n"
1716+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1717+
+ " LogicalAggregate(group=[{}], EXPR$0=[MIN($0)])\n"
1718+
+ " LogicalProject($f0=[0])\n"
1719+
+ " LogicalFilter(condition=[AND(=($7, $cor0.DEPTNO), =($1, 'SMITH'), >(CAST($cor0.DEPTNO):INTEGER NOT NULL, 0))])\n"
1720+
+ " LogicalTableScan(table=[[scott, EMP]])\n";
1721+
assertThat(before, hasTree(planBefore));
1722+
1723+
// Decorrelate without any rules, just "purely" decorrelation algorithm on RelDecorrelator
1724+
final RelNode after =
1725+
RelDecorrelator.decorrelateQuery(before, builder, RuleSets.ofList(Collections.emptyList()),
1726+
RuleSets.ofList(Collections.emptyList()));
1727+
final String planAfter = ""
1728+
+ "LogicalProject(DEPTNO=[$0], I0=[$3], I1=[$8])\n"
1729+
+ " LogicalJoin(condition=[AND(=($0, $6), =($5, $7))], joinType=[left])\n"
1730+
+ " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EXPR$0=[$5], DEPTNO0=[$0], $f5=[>(CAST($0):INTEGER NOT NULL, 0)])\n"
1731+
+ " LogicalJoin(condition=[=($3, $4)], joinType=[left])\n"
1732+
+ " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1733+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1734+
+ " LogicalAggregate(group=[{0}], EXPR$0=[MIN($1)])\n"
1735+
+ " LogicalProject(DEPTNO0=[$8], $f0=[1])\n"
1736+
+ " LogicalJoin(condition=[>($0, $8)], joinType=[inner])\n"
1737+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1738+
+ " LogicalAggregate(group=[{0}])\n"
1739+
+ " LogicalProject(DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1740+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1741+
+ " LogicalAggregate(group=[{0, 1}], EXPR$0=[MIN($2)])\n"
1742+
+ " LogicalProject(DEPTNO0=[$8], $f5=[$9], $f0=[0])\n"
1743+
+ " LogicalJoin(condition=[=($7, $8)], joinType=[inner])\n"
1744+
+ " LogicalFilter(condition=[=($1, 'SMITH')])\n"
1745+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1746+
+ " LogicalFilter(condition=[$1])\n"
1747+
+ " LogicalProject(DEPTNO=[$0], $f5=[>(CAST($0):INTEGER NOT NULL, 0)])\n"
1748+
+ " LogicalJoin(condition=[=($3, $4)], joinType=[left])\n"
1749+
+ " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1750+
+ " LogicalTableScan(table=[[scott, DEPT]])\n"
1751+
+ " LogicalAggregate(group=[{0}], EXPR$0=[MIN($1)])\n"
1752+
+ " LogicalProject(DEPTNO0=[$8], $f0=[1])\n"
1753+
+ " LogicalJoin(condition=[>($0, $8)], joinType=[inner])\n"
1754+
+ " LogicalTableScan(table=[[scott, EMP]])\n"
1755+
+ " LogicalAggregate(group=[{0}])\n"
1756+
+ " LogicalProject(DEPTNO0=[CAST($0):SMALLINT NOT NULL])\n"
1757+
+ " LogicalTableScan(table=[[scott, DEPT]])\n";
1758+
assertThat(after, hasTree(planAfter));
1759+
}
1760+
16721761
/** Test case for <a href="https://issues.apache.org/jira/browse/CALCITE-7320">[CALCITE-7320]
16731762
* AggregateProjectMergeRule throws AssertionError when Project maps multiple grouping keys
16741763
* 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
@@ -8967,4 +8967,79 @@ where e.deptno = 10;
89678967

89688968
!ok
89698969

8970+
# [CALCITE-5390] RelDecorrelator throws NullPointerException
8971+
# Verified against PostgreSQL.
8972+
select deptno,
8973+
(select min(1) from emp where empno > d.deptno) as i0,
8974+
(select min(0) from emp where deptno = d.deptno and ename = 'SMITH' and d.deptno > 0) as i1
8975+
from dept as d;
8976+
+--------+----+----+
8977+
| DEPTNO | I0 | I1 |
8978+
+--------+----+----+
8979+
| 10 | 1 | |
8980+
| 20 | 1 | 0 |
8981+
| 30 | 1 | |
8982+
| 40 | 1 | |
8983+
+--------+----+----+
8984+
(4 rows)
8985+
8986+
!ok
8987+
8988+
# [CALCITE-5390] RelDecorrelator throws NullPointerException
8989+
# Verified against PostgreSQL.
8990+
SELECT
8991+
(SELECT 1 FROM emp d WHERE d.job = a.job LIMIT 1) AS t1,
8992+
(SELECT a.job = 'PRESIDENT' FROM emp s LIMIT 1) as t2
8993+
FROM emp a;
8994+
+----+-------+
8995+
| T1 | T2 |
8996+
+----+-------+
8997+
| 1 | false |
8998+
| 1 | false |
8999+
| 1 | false |
9000+
| 1 | false |
9001+
| 1 | false |
9002+
| 1 | false |
9003+
| 1 | false |
9004+
| 1 | false |
9005+
| 1 | false |
9006+
| 1 | false |
9007+
| 1 | false |
9008+
| 1 | false |
9009+
| 1 | false |
9010+
| 1 | true |
9011+
+----+-------+
9012+
(14 rows)
9013+
9014+
!ok
9015+
9016+
# [CALCITE-5390] RelDecorrelator throws NullPointerException
9017+
# Verified against PostgreSQL.
9018+
SELECT *
9019+
FROM emp e
9020+
WHERE e.ename NOT IN (
9021+
SELECT d.dname
9022+
FROM dept d
9023+
WHERE e.deptno = d.deptno OR e.sal > 2000.0);
9024+
+-------+--------+-----------+------+------------+---------+---------+--------+
9025+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO |
9026+
+-------+--------+-----------+------+------------+---------+---------+--------+
9027+
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | | 20 |
9028+
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
9029+
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
9030+
| 7566 | JONES | MANAGER | 7839 | 1981-02-04 | 2975.00 | | 20 |
9031+
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
9032+
| 7698 | BLAKE | MANAGER | 7839 | 1981-01-05 | 2850.00 | | 30 |
9033+
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | | 10 |
9034+
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | | 20 |
9035+
| 7839 | KING | PRESIDENT | | 1981-11-17 | 5000.00 | | 10 |
9036+
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
9037+
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | | 20 |
9038+
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | | 30 |
9039+
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | | 20 |
9040+
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | | 10 |
9041+
+-------+--------+-----------+------+------------+---------+---------+--------+
9042+
(14 rows)
9043+
9044+
!ok
89709045
# End sub-query.iq

0 commit comments

Comments
 (0)