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

iwanttobepowerful 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 b1ba4e9aec [CALCITE-4693] Decorrelation drops rows for inner LATERAL 
join over aggregate without GROUP BY
b1ba4e9aec is described below

commit b1ba4e9aecae70586eaa8a442397c2bc3dda55e1
Author: Gian Merlino <[email protected]>
AuthorDate: Wed Oct 28 22:11:04 2026 -0700

    [CALCITE-4693] Decorrelation drops rows for inner LATERAL join over 
aggregate without GROUP BY
---
 .../apache/calcite/sql2rel/RelDecorrelator.java    |  15 +-
 .../sql2rel/TopDownGeneralDecorrelator.java        |   7 +-
 core/src/test/resources/sql/lateral.iq             | 225 +++++++++++++++++++++
 3 files changed, 238 insertions(+), 9 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java 
b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
index 02ec8afb66..585b9d0196 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -581,7 +581,7 @@ protected RexNode removeCorrelationExpr(
     }
 
     final RelNode oldInput = rel.getInput();
-    final Frame frame = getInvoke(oldInput, isCorVarDefined, rel, true);
+    final Frame frame = getInvoke(oldInput, isCorVarDefined, rel, 
parentPropagatesNullValues);
     if (frame == null) {
       // If input has not been rewritten, do not rewrite this rel.
       return null;
@@ -1910,8 +1910,7 @@ private static boolean isWidening(RelDataType type, 
RelDataType type1) {
 
     frameStack.push(Pair.of(rel.getCorrelationId(), leftFrame));
     final Frame rightFrame =
-        getInvoke(oldRight, true, rel,
-            rel.getJoinType() == JoinRelType.LEFT || 
parentPropagatesNullValues);
+        getInvoke(oldRight, true, rel, rel.getJoinType() == JoinRelType.LEFT);
     frameStack.pop();
 
     if (rightFrame == null || rightFrame.corDefOutputs.isEmpty()) {
@@ -2028,8 +2027,14 @@ private static boolean isWidening(RelDataType type, 
RelDataType type1) {
     final RelNode oldLeft = rel.getInput(0);
     final RelNode oldRight = rel.getInput(1);
 
-    final Frame leftFrame = getInvoke(oldLeft, isCorVarDefined, rel, 
parentPropagatesNullValues);
-    final Frame rightFrame = getInvoke(oldRight, isCorVarDefined, rel, 
parentPropagatesNullValues);
+    // Decorrelate recursively. Setting parentPropagatesNullValues lets a 
scalar aggregate return
+    // no row for a corVar value, rather than a row of NULLs. That is safe 
only for the side of a
+    // LEFT or RIGHT join that generates NULL values, since the join then 
supplies those NULLs.
+    // FULL does not qualify, because it must output the aggregate's row even 
when it has no match.
+    final Frame leftFrame =
+        getInvoke(oldLeft, isCorVarDefined, rel, rel.getJoinType() == 
JoinRelType.RIGHT);
+    final Frame rightFrame =
+        getInvoke(oldRight, isCorVarDefined, rel, rel.getJoinType() == 
JoinRelType.LEFT);
 
     if (leftFrame == null || rightFrame == null) {
       // If any input has not been rewritten, do not rewrite this rel.
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java 
b/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
index ead86414f2..8b31b5cdd1 100644
--- 
a/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
+++ 
b/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
@@ -330,7 +330,7 @@ private RelNode correlateElimination(RelNode rel, boolean 
allowEmptyOutputFromRe
 
     // decorrelate right side
     detectCorrelatedExpressions(correlate.getRight());
-    allowEmptyOutputFromRewrite &= correlate.getJoinType() != 
JoinRelType.LEFT_MARK;
+    allowEmptyOutputFromRewrite &= correlate.getJoinType() == JoinRelType.LEFT;
     RelNode newRight = unnest(correlate.getRight(), 
allowEmptyOutputFromRewrite);
     UnnestedQuery rightInfo = 
requireNonNull(mapRelToUnnestedQuery.get(correlate.getRight()));
 
@@ -748,7 +748,7 @@ public RelNode unnestInternal(Join join, boolean 
allowEmptyOutputFromRewrite) {
     } else {
       // when neither the left nor the right side has correlation,
       // but the join condition has correlation, domain D is pushed down to 
the left by default.
-      newLeft = unnest(join.getLeft(), allowEmptyOutputFromRewrite);
+      newLeft = unnest(join.getLeft(), join.getJoinType() == 
JoinRelType.RIGHT);
       pushDownToLeft = true;
       leftInfo = requireNonNull(mapRelToUnnestedQuery.get(join.getLeft()));
     }
@@ -770,8 +770,7 @@ public RelNode unnestInternal(Join join, boolean 
allowEmptyOutputFromRewrite) {
       rightInfo =
           new UnnestedQuery(join.getRight(), newRight, new TreeMap<>(), 
rightOldToNewOutputs);
     } else {
-      allowEmptyOutputFromRewrite &= join.getJoinType() != 
JoinRelType.LEFT_MARK;
-      newRight = unnest(join.getRight(), allowEmptyOutputFromRewrite);
+      newRight = unnest(join.getRight(), join.getJoinType() == 
JoinRelType.LEFT);
       pushDownToRight = true;
       rightInfo = requireNonNull(mapRelToUnnestedQuery.get(join.getRight()));
     }
diff --git a/core/src/test/resources/sql/lateral.iq 
b/core/src/test/resources/sql/lateral.iq
index 12fe348741..f560c78d67 100644
--- a/core/src/test/resources/sql/lateral.iq
+++ b/core/src/test/resources/sql/lateral.iq
@@ -331,4 +331,229 @@ EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], 
dir1=[ASC])
 
 !set planner-rules original
 
+# [CALCITE-4693] An aggregate without GROUP BY returns one row even for
+# empty input, so every department must appear.
+select d.deptno, t.s
+from dept as d
+cross join lateral (select sum(e.sal) as s from emp as e where e.deptno = 
d.deptno) as t
+order by d.deptno;
++--------+----------+
+| DEPTNO | S        |
++--------+----------+
+|     10 |  8750.00 |
+|     20 | 10875.00 |
+|     30 |  9400.00 |
+|     40 |          |
++--------+----------+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] As above, but with a LIMIT 1 in the subquery.
+select d.deptno, t.s
+from dept as d
+cross join lateral (select sum(e.sal) as s from emp as e where e.deptno = 
d.deptno limit 1) as t;
++--------+----------+
+| DEPTNO | S        |
++--------+----------+
+|     10 |  8750.00 |
+|     20 | 10875.00 |
+|     30 |  9400.00 |
+|     40 |          |
++--------+----------+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] The aggregate is the left input of an inner join inside a
+# LEFT JOIN LATERAL, so department 40 must have X = 1, not NULL.
+select d.deptno, t.s, t.x
+from dept as d
+left join lateral (
+  select a.s, b.x
+  from (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) as a
+  join (values (1)) as b(x) on true) as t on true
+order by d.deptno;
++--------+----------+---+
+| DEPTNO | S        | X |
++--------+----------+---+
+|     10 |  8750.00 | 1 |
+|     20 | 10875.00 | 1 |
+|     30 |  9400.00 | 1 |
+|     40 |          | 1 |
++--------+----------+---+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] As above, but the aggregate is the right input of the inner
+# join.
+select d.deptno, t.s, t.x
+from dept as d
+left join lateral (
+  select a.s, b.x
+  from (values (1)) as b(x)
+  join (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) as a 
on true) as t on true
+order by d.deptno;
++--------+----------+---+
+| DEPTNO | S        | X |
++--------+----------+---+
+|     10 |  8750.00 | 1 |
+|     20 | 10875.00 | 1 |
+|     30 |  9400.00 | 1 |
+|     40 |          | 1 |
++--------+----------+---+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] The aggregate is the preserved input of a LEFT JOIN, so
+# department 40 must have X = 1, not NULL.
+select d.deptno, t.s, t.x
+from dept as d
+cross join lateral (
+  select a.s, b.x
+  from (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) as a
+  left join (values (1)) as b(x) on true) as t
+order by d.deptno;
++--------+----------+---+
+| DEPTNO | S        | X |
++--------+----------+---+
+|     10 |  8750.00 | 1 |
+|     20 | 10875.00 | 1 |
+|     30 |  9400.00 | 1 |
+|     40 |          | 1 |
++--------+----------+---+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] The aggregate is the null-generating input of a LEFT JOIN,
+# where no row has the same effect as a row of nulls.
+select d.deptno, t.x, t.s
+from dept as d
+cross join lateral (
+  select b.x, a.s
+  from (values (1)) as b(x)
+  left join (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) 
as a on true) as t
+order by d.deptno;
++--------+---+----------+
+| DEPTNO | X | S        |
++--------+---+----------+
+|     10 | 1 |  8750.00 |
+|     20 | 1 | 10875.00 |
+|     30 | 1 |  9400.00 |
+|     40 | 1 |          |
++--------+---+----------+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] As above, but with a condition on the aggregate.
+select d.deptno, t.x, t.s
+from dept as d
+cross join lateral (
+  select b.x, a.s
+  from (values (1)) as b(x)
+  left join (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) 
as a
+    on a.s > 9000) as t
+order by d.deptno;
++--------+---+----------+
+| DEPTNO | X | S        |
++--------+---+----------+
+|     10 | 1 |          |
+|     20 | 1 | 10875.00 |
+|     30 | 1 |  9400.00 |
+|     40 | 1 |          |
++--------+---+----------+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] As above, but with COUNT, which is 0 rather than NULL for
+# department 40.
+select d.deptno, t.x, t.c
+from dept as d
+cross join lateral (
+  select b.x, a.c
+  from (values (1)) as b(x)
+  left join (select count(*) as c from emp as e where e.deptno = d.deptno) as 
a on true) as t
+order by d.deptno;
++--------+---+---+
+| DEPTNO | X | C |
++--------+---+---+
+|     10 | 1 | 3 |
+|     20 | 1 | 5 |
+|     30 | 1 | 6 |
+|     40 | 1 | 0 |
++--------+---+---+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] The aggregate is the null-generating input of a RIGHT JOIN.
+select d.deptno, t.s, t.x
+from dept as d
+cross join lateral (
+  select a.s, b.x
+  from (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) as a
+  right join (values (1)) as b(x) on true) as t
+order by d.deptno;
++--------+----------+---+
+| DEPTNO | S        | X |
++--------+----------+---+
+|     10 |  8750.00 | 1 |
+|     20 | 10875.00 | 1 |
+|     30 |  9400.00 | 1 |
+|     40 |          | 1 |
++--------+----------+---+
+(4 rows)
+
+!ok
+
+# [CALCITE-4693] The aggregate is an input of a FULL JOIN, which preserves it
+# as well as generating nulls for it, so department 40 must have both a row
+# with X = 1 and an unmatched row with X = NULL.
+select d.deptno, t.s, t.x
+from dept as d
+cross join lateral (
+  select a.s, b.x
+  from (select sum(e.sal) as s from emp as e where e.deptno = d.deptno) as a
+  full join (values (1)) as b(x) on a.s > 9000) as t
+order by d.deptno, t.s, t.x;
++--------+----------+---+
+| DEPTNO | S        | X |
++--------+----------+---+
+|     10 |  8750.00 |   |
+|     10 |          | 1 |
+|     20 | 10875.00 | 1 |
+|     30 |  9400.00 | 1 |
+|     40 |          | 1 |
+|     40 |          |   |
++--------+----------+---+
+(6 rows)
+
+!ok
+
+# [CALCITE-4693] An aggregate without GROUP BY returns one row even for
+# empty input, so every department must appear.
+!use post
+select k.deptno,
+  k.deptno - t2.totalAmount as amountDue
+from dept as k,
+  lateral (select sum(emp.deptno) as totalAmount
+    from emp
+    where emp.deptno = k.deptno) as t2;
++--------+-----------+
+| DEPTNO | AMOUNTDUE |
++--------+-----------+
+|     10 |       -10 |
+|     20 |         0 |
+|     30 |       -30 |
+|     40 |           |
++--------+-----------+
+(4 rows)
+
+!ok
+
 # End lateral.iq

Reply via email to