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 c91b6f4db8 [CALCITE-7587] RelDecorrelator fails on correlated scalar 
subquery with ROW_NUMBER window function due to RexOver nullability mismatch
c91b6f4db8 is described below

commit c91b6f4db8188559237393962187db9557dc16bb
Author: Weihua Zhang <[email protected]>
AuthorDate: Fri Jun 12 14:21:01 2026 +0800

    [CALCITE-7587] RelDecorrelator fails on correlated scalar subquery with 
ROW_NUMBER window function due to RexOver nullability mismatch
---
 .../apache/calcite/sql2rel/RelDecorrelator.java    | 112 +++++++++++++++++++++
 core/src/test/resources/sql/sub-query.iq           |  33 ++++++
 2 files changed, 145 insertions(+)

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 f9e7049af2..a5a8175793 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -2695,6 +2695,118 @@ private RexNode createCaseExpression(
       return literal;
     }
 
+    /**
+     * Decorrelates a window expression ({@link RexOver}) that may reference
+     * correlation variables in its {@code PARTITION BY} / {@code ORDER BY}
+     * keys (or the aggregate arguments).
+     *
+     * <p>For each correlation field reachable from {@code over}, we:
+     * <ol>
+     *   <li>rewrite it to an input reference of the (already decorrelated)
+     *       left-hand side of the surrounding correlate, and</li>
+     *   <li>append that reference to the window's {@code partitionKeys}
+     *       (if not already present), so that the window is evaluated
+     *       independently per outer row group, matching the original
+     *       per-correlated-row semantics.</li>
+     * </ol>
+     *
+     * <p>If the scalar sub-query has been pulled above a LEFT correlate, the
+     * result is wrapped in a {@code CASE} on the null-indicator so that it
+     * stays {@code NULL} when the right side did not match.
+     *
+     * <p>Concrete example. For the SQL:
+     * <pre>{@code
+     * SELECT e.ename,
+     *        (SELECT ROW_NUMBER() OVER (PARTITION BY e.deptno
+     *                                   ORDER BY e.empno, d.deptno)
+     *           FROM dept d WHERE e.deptno = d.deptno) AS rn
+     * FROM   emp e
+     * ORDER BY e.empno
+     * }</pre>
+     *
+     * <p>BEFORE this method (window expression as seen on entry):
+     * <pre>{@code
+     * ROW_NUMBER() OVER (
+     *   PARTITION BY $cor2.DEPTNO
+     *   ORDER BY    $cor2.EMPNO, $0)
+     *   partitionKeys = [$cor2.DEPTNO]
+     *   orderKeys     = [$cor2.EMPNO, $0]
+     * }</pre>
+     *
+     * <p>AFTER this method (with {@code projectPulledAboveLeftCorrelator=true}
+     * and a null-indicator at column {@code $3}; correlation fields are
+     * rewritten to input refs from the outer side and {@code $cor2.EMPNO} is
+     * additionally appended to the partition keys):
+     * <pre>{@code
+     * CASE(IS NULL($3), null:BIGINT,
+     *      CAST(
+     *        ROW_NUMBER() OVER (
+     *          PARTITION BY CASE(IS NULL($3), null:TINYINT,  
CAST($2):TINYINT),
+     *                       CASE(IS NULL($3), null:SMALLINT, 
CAST($0):SMALLINT)
+     *          ORDER BY    CASE(IS NULL($3), null:SMALLINT, 
CAST($0):SMALLINT),
+     *                       $3)
+     *      ):BIGINT)
+     *   newOver.partitionKeys =
+     *     [CASE(IS NULL($3), null:TINYINT,  CAST($2):TINYINT),
+     *      CASE(IS NULL($3), null:SMALLINT, CAST($0):SMALLINT)]
+     *   newOver.orderKeys     =
+     *     [CASE(IS NULL($3), null:SMALLINT, CAST($0):SMALLINT), $3]
+     * }</pre>
+     *
+     * <p>Note that {@code $cor2.EMPNO} only appeared in the original
+     * {@code ORDER BY}; without appending its decorrelated form to
+     * {@code partitionKeys} the rewritten window would silently widen its
+     * computation scope across outer rows and produce wrong results.
+     */
+    @Override public RexNode visitOver(RexOver over) {
+      // Collect correlation fields that are referenced directly by the window
+      // expression. They need to be added to the window partition keys so that
+      // decorrelation does not widen the window computation scope.
+      final List<RexFieldAccess> correlationFields = new ArrayList<>();
+      over.accept(new RexVisitorImpl<Void>(true) {
+        @Override public Void visitFieldAccess(RexFieldAccess fieldAccess) {
+          if (cm.mapFieldAccessToCorRef.containsKey(fieldAccess)
+              && !correlationFields.contains(fieldAccess)) {
+            correlationFields.add(fieldAccess);
+          }
+          return super.visitFieldAccess(fieldAccess);
+        }
+      });
+
+      RexOver newOver = (RexOver) super.visitOver(over);
+      if (!correlationFields.isEmpty()) {
+        final List<RexNode> partitionKeys = new 
ArrayList<>(newOver.getWindow().partitionKeys);
+        boolean update = false;
+        for (RexFieldAccess fieldAccess : correlationFields) {
+          // Rewrite the correlation field to its decorrelated input reference,
+          // then use it as an additional partition key for the window.
+          RexNode partitionKey = visitFieldAccess(fieldAccess);
+          if (!partitionKeys.contains(partitionKey)) {
+            partitionKeys.add(partitionKey);
+            update = true;
+          }
+        }
+        if (update) {
+          newOver =
+              (RexOver) rexBuilder.makeOver(newOver.getParserPosition(),
+                  newOver.getType(),
+                  newOver.getAggOperator(), newOver.getOperands(), 
partitionKeys,
+                  newOver.getWindow().orderKeys,
+                  newOver.getWindow().getLowerBound(),
+                  newOver.getWindow().getUpperBound(),
+                  newOver.getWindow().getExclude(),
+                  newOver.getWindow().isRows(), true, false, 
newOver.isDistinct(),
+                  newOver.ignoreNulls());
+        }
+      }
+      if (projectPulledAboveLeftCorrelator && (nullIndicator != null)) {
+        // Once a scalar sub-query is pulled above a left correlate, the result
+        // must remain nullable when there is no matching row on the right 
side.
+        return createCaseExpression(nullIndicator, null, newOver);
+      }
+      return newOver;
+    }
+
     @Override public RexNode visitCall(final RexCall call) {
       RexNode newCall;
 
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index db69258df2..4d4398cfc4 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -8168,6 +8168,39 @@ SELECT deptno FROM dept WHERE 1000.00 >
 
 !ok
 
+# [CALCITE-7587] RelDecorrelator fails on correlated scalar subquery with 
ROW_NUMBER window function
+# due to RexOver nullability mismatch
+# Correlated scalar sub-query in the SELECT list that contains a window 
function.
+# Decorrelation must add the correlation key to the window partition and 
preserve nullability
+# of the OVER expression after pulling it above the left correlate.
+SELECT e.ename,
+  (SELECT ROW_NUMBER() OVER (PARTITION BY e.deptno ORDER BY e.empno, d.deptno)
+   FROM dept d
+   WHERE e.deptno = d.deptno) AS rn
+FROM emp e
+ORDER BY e.empno;
++--------+----+
+| ENAME  | RN |
++--------+----+
+| SMITH  |  1 |
+| ALLEN  |  1 |
+| WARD   |  1 |
+| JONES  |  1 |
+| MARTIN |  1 |
+| BLAKE  |  1 |
+| CLARK  |  1 |
+| SCOTT  |  1 |
+| KING   |  1 |
+| TURNER |  1 |
+| ADAMS  |  1 |
+| JAMES  |  1 |
+| FORD   |  1 |
+| MILLER |  1 |
++--------+----+
+(14 rows)
+
+!ok
+
 # [CALCITE-7584] RelDecorrelator produces incorrect results for correlated 
LATERAL sub-queries with window functions
 # Correlated LATERAL sub-query with a window expression.
 # The equality predicate between the inner and outer query must remain applied

Reply via email to