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

xuzifu666 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 9abd95b7fc [CALCITE-7620] Result of FILTER clause in window functions 
is incorrect
9abd95b7fc is described below

commit 9abd95b7fc15e45220ed56cc70a17f22f22aa280
Author: Yu Xu <[email protected]>
AuthorDate: Tue Jun 23 11:42:37 2026 +0800

    [CALCITE-7620] Result of FILTER clause in window functions is incorrect
---
 .../calcite/rel/metadata/RelMdCollation.java       |  11 +-
 .../calcite/rel/rules/ProjectToWindowRule.java     |   3 +
 .../apache/calcite/sql2rel/SqlToRelConverter.java  |  62 +++++++++++
 core/src/test/resources/sql/sub-query.iq           |  22 ++--
 core/src/test/resources/sql/winagg.iq              | 124 +++++++++++++--------
 5 files changed, 163 insertions(+), 59 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdCollation.java 
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdCollation.java
index 6ffe9c1f62..e6ae979b1a 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdCollation.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdCollation.java
@@ -66,6 +66,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -372,13 +373,13 @@ public static List<RelCollation> sort(RelCollation 
collation) {
   /** Helper method to determine a
    * {@link org.apache.calcite.rel.core.Window}'s collation.
    *
-   * <p>A Window projects the fields of its input first, followed by the output
-   * from each of its windows. Assuming (quite reasonably) that the
-   * implementation does not re-order its input rows, then any collations of 
its
-   * input are preserved. */
+   * <p>A Window operator groups rows by PARTITION BY keys and sorts each
+   * partition by ORDER BY keys. The output order is therefore not defined by
+   * a simple collation in the general case, so we conservatively report no
+   * collations. */
   public static @Nullable List<RelCollation> window(RelMetadataQuery mq, 
RelNode input,
       ImmutableList<Window.Group> groups) {
-    return mq.collations(input);
+    return Collections.emptyList();
   }
 
   /** Helper method to determine a
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java
index 536f5834e4..14a333aace 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ProjectToWindowRule.java
@@ -20,6 +20,7 @@
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Calc;
 import org.apache.calcite.rel.core.Project;
@@ -53,6 +54,7 @@
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.List;
@@ -262,6 +264,7 @@ static class WindowedAggRelSplitter extends CalcRelSplitter 
{
               RelBuilder relBuilder, RelNode input, RexProgram program, 
List<RelHint> hints) {
             checkArgument(program.getCondition() == null,
                 "WindowedAggregateRel cannot accept a condition");
+            traitSet = traitSet.replaceIfs(RelCollationTraitDef.INSTANCE, 
Collections::emptyList);
             return LogicalWindow.create(cluster, traitSet, relBuilder, input,
                 program, hints);
           }
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 1551d1630d..507c144288 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -2504,6 +2504,11 @@ private RexNode convertOver(Blackboard bb, SqlNode node) 
{
     SqlCall call = (SqlCall) node;
     bb.getValidator().deriveType(bb.scope, call);
     SqlCall aggCall = call.operand(0);
+    @Nullable SqlNode filter = null;
+    if (aggCall.getKind() == SqlKind.FILTER) {
+      filter = aggCall.operand(1);
+      aggCall = aggCall.operand(0);
+    }
     boolean ignoreNulls = false;
     switch (aggCall.getKind()) {
     case IGNORE_NULLS:
@@ -2515,6 +2520,22 @@ private RexNode convertOver(Blackboard bb, SqlNode node) 
{
     default:
       break;
     }
+    if (filter != null) {
+      final SqlOperator op = aggCall.getOperator();
+      if (op instanceof SqlAggFunction
+          && !((SqlAggFunction) op).requiresOver()) {
+        // FILTER on a windowed aggregate can be implemented by wrapping the
+        // aggregate arguments in CASE expressions, because true aggregates
+        // ignore NULL inputs. This does not work for window value functions
+        // (FIRST_VALUE, LAST_VALUE, NTH_VALUE, LEAD, LAG, etc.) which do not
+        // ignore NULL inputs.
+        aggCall = applyFilterToAggArgs(aggCall, filter);
+        bb.getValidator().deriveType(bb.scope, aggCall);
+      } else {
+        throw new UnsupportedOperationException(
+            "FILTER clause is not supported for window function " + 
op.getName());
+      }
+    }
 
     SqlNode windowOrRef = call.operand(1);
     final SqlWindow window =
@@ -2609,6 +2630,47 @@ private RexNode convertOver(Blackboard bb, SqlNode node) 
{
     }
   }
 
+  /**
+   * Applies a FILTER clause to the arguments of an aggregate call by wrapping
+   * each argument in a CASE expression. For example,
+   * {@code SUM(sal) FILTER (WHERE comm IS NOT NULL)} becomes
+   * {@code SUM(CASE WHEN comm IS NOT NULL THEN sal END)}.
+   *
+   * <p>This transformation preserves the semantics of the FILTER clause for
+   * windowed aggregates: rows that do not satisfy the filter contribute NULL
+   * and are ignored by the aggregate function.
+   */
+  private static SqlCall applyFilterToAggArgs(SqlCall aggCall, SqlNode filter) 
{
+    final SqlOperator op = aggCall.getOperator();
+    final List<SqlNode> operands = aggCall.getOperandList();
+    final SqlParserPos pos = aggCall.getParserPosition();
+    final SqlLiteral quantifier = aggCall.getFunctionQuantifier();
+    final List<SqlNode> newOperands = new ArrayList<>(operands.size());
+    if (op == SqlStdOperatorTable.COUNT
+        && operands.size() == 1
+        && operands.get(0) instanceof SqlIdentifier
+        && ((SqlIdentifier) operands.get(0)).isStar()) {
+      // COUNT(*) FILTER (WHERE x) => COUNT(CASE WHEN x THEN 0 END)
+      newOperands.add(
+          new SqlCase(pos, null, SqlNodeList.of(filter),
+              SqlNodeList.of(SqlLiteral.createExactNumeric("0", pos)),
+              SqlLiteral.createNull(pos)));
+    } else {
+      for (SqlNode operand : operands) {
+        if (operand instanceof SqlIdentifier
+            && ((SqlIdentifier) operand).isStar()) {
+          newOperands.add(operand);
+        } else {
+          newOperands.add(
+              new SqlCase(pos, null, SqlNodeList.of(filter),
+                  SqlNodeList.of(operand),
+                  SqlLiteral.createNull(pos)));
+        }
+      }
+    }
+    return op.createCall(quantifier, pos, newOperands);
+  }
+
   protected void convertFrom(
       Blackboard bb,
       @Nullable SqlNode from) {
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 2f190ed040..84f9aa5183 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -2430,11 +2430,12 @@ EnumerableCalc(expr#0..5=[{inputs}], expr#6=[RAND()], 
expr#7=[CAST($t6):INTEGER
     EnumerableSort(sort0=[$2], dir0=[ASC])
       EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], SAL=[$t5], 
DEPTNO=[$t7])
         EnumerableTableScan(table=[[scott, EMP]])
-    EnumerableSort(sort0=[$1], dir0=[ASC])
-      EnumerableCalc(expr#0..1=[{inputs}], expr#2=[false], expr#3=[1], 
expr#4=[<=($t1, $t3)], cs=[$t2], DEPTNO=[$t0], rn=[$t1], $condition=[$t4])
-        EnumerableWindow(window#0=[window(partition {0} rows between UNBOUNDED 
PRECEDING and CURRENT ROW aggs [ROW_NUMBER()])], constants=[[false]])
-          EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0])
-            EnumerableTableScan(table=[[scott, DEPT]])
+    EnumerableCalc(expr#0..2=[{inputs}], expr#3=[1], expr#4=[<=($t2, $t3)], 
proj#0..2=[{exprs}], $condition=[$t4])
+      EnumerableSort(sort0=[$1], dir0=[ASC])
+        EnumerableCalc(expr#0..1=[{inputs}], expr#2=[false], cs=[$t2], 
DEPTNO=[$t0], rn=[$t1])
+          EnumerableWindow(window#0=[window(partition {0} rows between 
UNBOUNDED PRECEDING and CURRENT ROW aggs [ROW_NUMBER()])], constants=[[false]])
+            EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0])
+              EnumerableTableScan(table=[[scott, DEPT]])
 !plan
 !}
 
@@ -2540,11 +2541,12 @@ EnumerableCalc(expr#0..5=[{inputs}], expr#6=[NOT($t3)], 
expr#7=[IS NOT NULL($t3)
     EnumerableSort(sort0=[$2], dir0=[ASC])
       EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], SAL=[$t5], 
DEPTNO=[$t7])
         EnumerableTableScan(table=[[scott, EMP]])
-    EnumerableSort(sort0=[$1], dir0=[ASC])
-      EnumerableCalc(expr#0..1=[{inputs}], expr#2=[false], expr#3=[1], 
expr#4=[<=($t1, $t3)], cs=[$t2], DEPTNO=[$t0], rn=[$t1], $condition=[$t4])
-        EnumerableWindow(window#0=[window(partition {0} rows between UNBOUNDED 
PRECEDING and CURRENT ROW aggs [ROW_NUMBER()])], constants=[[false]])
-          EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0])
-            EnumerableTableScan(table=[[scott, DEPT]])
+    EnumerableCalc(expr#0..2=[{inputs}], expr#3=[1], expr#4=[<=($t2, $t3)], 
proj#0..2=[{exprs}], $condition=[$t4])
+      EnumerableSort(sort0=[$1], dir0=[ASC])
+        EnumerableCalc(expr#0..1=[{inputs}], expr#2=[false], cs=[$t2], 
DEPTNO=[$t0], rn=[$t1])
+          EnumerableWindow(window#0=[window(partition {0} rows between 
UNBOUNDED PRECEDING and CURRENT ROW aggs [ROW_NUMBER()])], constants=[[false]])
+            EnumerableCalc(expr#0..2=[{inputs}], DEPTNO=[$t0])
+              EnumerableTableScan(table=[[scott, DEPT]])
 !plan
 !}
 
diff --git a/core/src/test/resources/sql/winagg.iq 
b/core/src/test/resources/sql/winagg.iq
index d8348da422..1f07d4351d 100644
--- a/core/src/test/resources/sql/winagg.iq
+++ b/core/src/test/resources/sql/winagg.iq
@@ -1186,20 +1186,20 @@ order by empno;
 +-------+--------+----------------+
 | EMPNO | DEPTNO | FILTERED_COUNT |
 +-------+--------+----------------+
-|  7369 |     20 |              0 |
-|  7566 |     20 |              5 |
-|  7788 |     20 |              5 |
-|  7876 |     20 |              0 |
-|  7902 |     20 |              5 |
-|  7782 |     10 |              3 |
-|  7839 |     10 |              3 |
-|  7934 |     10 |              0 |
-|  7499 |     30 |              6 |
-|  7521 |     30 |              0 |
-|  7654 |     30 |              0 |
-|  7698 |     30 |              6 |
-|  7844 |     30 |              0 |
-|  7900 |     30 |              0 |
+|  7369 |     20 |              3 |
+|  7499 |     30 |              2 |
+|  7521 |     30 |              2 |
+|  7566 |     20 |              3 |
+|  7654 |     30 |              2 |
+|  7698 |     30 |              2 |
+|  7782 |     10 |              2 |
+|  7788 |     20 |              3 |
+|  7839 |     10 |              2 |
+|  7844 |     30 |              2 |
+|  7876 |     20 |              3 |
+|  7900 |     30 |              2 |
+|  7902 |     20 |              3 |
+|  7934 |     10 |              2 |
 +-------+--------+----------------+
 (14 rows)
 
@@ -1214,19 +1214,19 @@ order by empno;
 | EMPNO | DEPTNO | FILTERED_SUM |
 +-------+--------+--------------+
 |  7369 |     20 |              |
+|  7499 |     30 |      5600.00 |
+|  7521 |     30 |      5600.00 |
 |  7566 |     20 |              |
+|  7654 |     30 |      5600.00 |
+|  7698 |     30 |      5600.00 |
+|  7782 |     10 |              |
 |  7788 |     20 |              |
+|  7839 |     10 |              |
+|  7844 |     30 |      5600.00 |
 |  7876 |     20 |              |
+|  7900 |     30 |      5600.00 |
 |  7902 |     20 |              |
-|  7782 |     10 |              |
-|  7839 |     10 |              |
 |  7934 |     10 |              |
-|  7499 |     30 |      9400.00 |
-|  7521 |     30 |      9400.00 |
-|  7654 |     30 |      9400.00 |
-|  7698 |     30 |              |
-|  7844 |     30 |      9400.00 |
-|  7900 |     30 |              |
 +-------+--------+--------------+
 (14 rows)
 
@@ -1241,20 +1241,20 @@ order by empno;
 +-------+--------+----------------+-------------+
 | EMPNO | DEPTNO | HIGH_SAL_COUNT | LOW_SAL_SUM |
 +-------+--------+----------------+-------------+
-|  7369 |     20 |              0 |    10875.00 |
-|  7566 |     20 |              5 |             |
-|  7788 |     20 |              5 |             |
-|  7876 |     20 |              0 |    10875.00 |
-|  7902 |     20 |              5 |             |
-|  7782 |     10 |              3 |             |
-|  7839 |     10 |              3 |             |
-|  7934 |     10 |              0 |     8750.00 |
-|  7499 |     30 |              6 |             |
-|  7521 |     30 |              0 |     9400.00 |
-|  7654 |     30 |              0 |     9400.00 |
-|  7698 |     30 |              6 |             |
-|  7844 |     30 |              0 |     9400.00 |
-|  7900 |     30 |              0 |     9400.00 |
+|  7369 |     20 |              3 |     1900.00 |
+|  7499 |     30 |              2 |     4950.00 |
+|  7521 |     30 |              2 |     4950.00 |
+|  7566 |     20 |              3 |     1900.00 |
+|  7654 |     30 |              2 |     4950.00 |
+|  7698 |     30 |              2 |     4950.00 |
+|  7782 |     10 |              2 |     1300.00 |
+|  7788 |     20 |              3 |     1900.00 |
+|  7839 |     10 |              2 |     1300.00 |
+|  7844 |     30 |              2 |     4950.00 |
+|  7876 |     20 |              3 |     1900.00 |
+|  7900 |     30 |              2 |     4950.00 |
+|  7902 |     20 |              3 |     1900.00 |
+|  7934 |     10 |              2 |     1300.00 |
 +-------+--------+----------------+-------------+
 (14 rows)
 
@@ -1269,22 +1269,58 @@ order by empno;
 | EMPNO | DEPTNO | SAL     | RUNNING_SUM |
 +-------+--------+---------+-------------+
 |  7369 |     20 |  800.00 |             |
-|  7566 |     20 | 2975.00 |     3775.00 |
-|  7788 |     20 | 3000.00 |     6775.00 |
-|  7876 |     20 | 1100.00 |     7875.00 |
-|  7902 |     20 | 3000.00 |    10875.00 |
-|  7782 |     10 | 2450.00 |     2450.00 |
-|  7839 |     10 | 5000.00 |     7450.00 |
-|  7934 |     10 | 1300.00 |     8750.00 |
 |  7499 |     30 | 1600.00 |     1600.00 |
 |  7521 |     30 | 1250.00 |     2850.00 |
+|  7566 |     20 | 2975.00 |     2975.00 |
 |  7654 |     30 | 1250.00 |     4100.00 |
 |  7698 |     30 | 2850.00 |     6950.00 |
+|  7782 |     10 | 2450.00 |     2450.00 |
+|  7788 |     20 | 3000.00 |     5975.00 |
+|  7839 |     10 | 5000.00 |     7450.00 |
 |  7844 |     30 | 1500.00 |     8450.00 |
-|  7900 |     30 |  950.00 |             |
+|  7876 |     20 | 1100.00 |     7075.00 |
+|  7900 |     30 |  950.00 |     8450.00 |
+|  7902 |     20 | 3000.00 |    10075.00 |
+|  7934 |     10 | 1300.00 |     8750.00 |
 +-------+--------+---------+-------------+
 (14 rows)
 
 !ok
 
+# Test 5: FILTER with OVER and running window without PARTITION BY
+select ename, job, hiredate,
+  avg(sal) over (order by hiredate, ename rows 3 preceding) as avg_sal,
+  avg(sal) filter (where job = 'MANAGER') over (order by hiredate, ename rows 
3 preceding)
+    as avg_mgr_sal
+from emp
+order by hiredate, ename;
++--------+-----------+------------+---------+-------------+
+| ENAME  | JOB       | HIREDATE   | AVG_SAL | AVG_MGR_SAL |
++--------+-----------+------------+---------+-------------+
+| SMITH  | CLERK     | 1980-12-17 |  800.00 |             |
+| BLAKE  | MANAGER   | 1981-01-05 | 1825.00 |     2850.00 |
+| JONES  | MANAGER   | 1981-02-04 | 2208.33 |     2912.50 |
+| ALLEN  | SALESMAN  | 1981-02-20 | 2056.25 |     2912.50 |
+| WARD   | SALESMAN  | 1981-02-22 | 2168.75 |     2912.50 |
+| CLARK  | MANAGER   | 1981-06-09 | 2068.75 |     2712.50 |
+| TURNER | SALESMAN  | 1981-09-08 | 1700.00 |     2450.00 |
+| MARTIN | SALESMAN  | 1981-09-28 | 1612.50 |     2450.00 |
+| KING   | PRESIDENT | 1981-11-17 | 2550.00 |     2450.00 |
+| FORD   | ANALYST   | 1981-12-03 | 2687.50 |             |
+| JAMES  | CLERK     | 1981-12-03 | 2550.00 |             |
+| MILLER | CLERK     | 1982-01-23 | 2562.50 |             |
+| SCOTT  | ANALYST   | 1987-04-19 | 2062.50 |             |
+| ADAMS  | CLERK     | 1987-05-23 | 1587.50 |             |
++--------+-----------+------------+---------+-------------+
+(14 rows)
+
+!ok
+
+# Test 6: FILTER on window value functions is not supported
+select first_value(sal) filter (where job = 'MANAGER') over (order by hiredate)
+from emp;
+java.sql.SQLException: Error while executing SQL "select first_value(sal) 
filter (where job = 'MANAGER') over (order by hiredate)
+from emp": FILTER clause is not supported for window function FIRST_VALUE
+!error
+
 # End winagg.iq

Reply via email to