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 6e08166896 [CALCITE-7595] Support FILTER clause with window functions
6e08166896 is described below
commit 6e08166896934be46afcb841aaf62a771c3814db
Author: Yu Xu <[email protected]>
AuthorDate: Tue Jun 9 18:19:50 2026 +0800
[CALCITE-7595] Support FILTER clause with window functions
---
.../adapter/enumerable/EnumerableWindow.java | 10 +-
.../calcite/adapter/enumerable/RexImpTable.java | 20 ++++
.../org/apache/calcite/sql/SqlOverOperator.java | 19 +++-
.../org/apache/calcite/test/SqlValidatorTest.java | 24 ++++-
core/src/test/resources/sql/winagg.iq | 114 +++++++++++++++++++++
site/_docs/reference.md | 11 ++
6 files changed, 192 insertions(+), 6 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
index 6197420b05..78ecce8821 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
@@ -451,6 +451,7 @@ private static void sampleOfTheGeneratedWindowedAggregate()
{
hasRows, frameRowCount, partitionRowCount,
jDecl, inputPhysTypeFinal);
+ final RelDataType inputRowType = inputPhysType.getRowType();
final Function<AggImpState, List<RexNode>> rexArguments = agg -> {
List<Integer> argList = agg.call.getArgList();
List<RelDataType> inputTypes =
@@ -464,7 +465,7 @@ private static void sampleOfTheGeneratedWindowedAggregate()
{
return args;
};
- implementAdd(aggs, builder7, resultContextBuilder, rexArguments, jDecl);
+ implementAdd(aggs, builder7, resultContextBuilder, rexArguments, jDecl,
inputRowType);
BlockStatement forBlock = builder7.toBlock();
// Don't run the aggregate function if current row is excluded
@@ -866,7 +867,8 @@ private static void implementAdd(List<AggImpState> aggs,
final BlockBuilder builder7,
final Function<BlockBuilder, WinAggFrameResultContext> frame,
final Function<AggImpState, List<RexNode>> rexArguments,
- final DeclarationStatement jDecl) {
+ final DeclarationStatement jDecl,
+ final RelDataType inputRowType) {
for (final AggImpState agg : aggs) {
final WinAggAddContext addContext =
new WinAggAddContextImpl(builder7, requireNonNull(agg.state,
"agg.state"), frame) {
@@ -879,7 +881,9 @@ private static void implementAdd(List<AggImpState> aggs,
}
@Override public @Nullable RexNode rexFilterArgument() {
- return null; // REVIEW
+ return agg.call.filterArg < 0
+ ? null
+ : RexInputRef.of(agg.call.filterArg, inputRowType);
}
};
agg.implementor.implementAdd(requireNonNull(agg.context, "agg.context"),
addContext);
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 549bddbf72..c78336e172 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -417,6 +417,7 @@
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.EVERY;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.EXP;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.EXTRACT;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.FILTER;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.FIRST_VALUE;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.FLOOR;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.FUSION;
@@ -1250,6 +1251,11 @@ void populate2() {
NotJsonImplementor.of(
new MethodImplementor(BuiltInMethod.IS_JSON_SCALAR.method,
NullPolicy.NONE, false)));
+ // Generates conditional expressions for aggregate FILTER clause:
+ // e.g. SUM(salary) FILTER (WHERE dept='Sales') → condition ? sum : NULL
+ // FilterImplementor is used for all FILTER operations, but the
restriction only affects
+ // OVER clause, because normal aggregates never pass through
SqlOverOperator.
+ define(FILTER, new FilterImplementor());
}
/** Third step of population. */
@@ -5111,4 +5117,18 @@ private static class ReplaceImplementor extends
AbstractRexCallImplementor {
operand0, operand1, operand2, Expressions.constant(isCaseSensitive));
}
}
+
+ /** Implementor for the FILTER operator. */
+ private static class FilterImplementor extends AbstractRexCallImplementor {
+ FilterImplementor() {
+ super("filter", NullPolicy.NONE, false);
+ }
+
+ @Override Expression implementSafe(RexToLixTranslator translator, RexCall
call,
+ List<Expression> argValueList) {
+ final Expression value = argValueList.get(0);
+ final Expression condition = argValueList.get(1);
+ return Expressions.condition(condition, value, NULL_EXPR);
+ }
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
b/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
index cc42a9ad0c..b1ed13808d 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlOverOperator.java
@@ -64,9 +64,14 @@ public SqlOverOperator() {
assert call.getOperator() == this;
assert call.operandCount() == 2;
SqlCall aggCall = call.operand(0);
+ boolean hasFilter = false;
switch (aggCall.getKind()) {
case RESPECT_NULLS:
case IGNORE_NULLS:
+ case FILTER:
+ if (aggCall.getKind() == SqlKind.FILTER) {
+ hasFilter = true;
+ }
validator.validateCall(aggCall, scope);
aggCall = aggCall.operand(0);
break;
@@ -76,6 +81,11 @@ public SqlOverOperator() {
if (!aggCall.getOperator().isAggregator()) {
throw validator.newValidationError(aggCall, RESOURCE.overNonAggregate());
}
+ // COUNT(DISTINCT) is not allowed in window functions with FILTER
+ if (hasFilter && aggCall.getKind() == SqlKind.COUNT
+ && aggCall.getFunctionQuantifier() != null) {
+ throw validator.newValidationError(aggCall, RESOURCE.overNonAggregate());
+ }
final SqlNode window = call.operand(1);
validator.validateWindow(window, scope, aggCall);
}
@@ -102,7 +112,14 @@ public SqlOverOperator() {
SqlNode window = call.operand(1);
SqlWindow w = validator.resolveWindow(window, scope);
- final SqlCall aggCall = (SqlCall) agg;
+ SqlCall aggCall = (SqlCall) agg;
+ // Unwrap FILTER, RESPECT_NULLS, or IGNORE_NULLS to get the actual
aggregate call
+ while (aggCall != null
+ && (aggCall.getKind() == SqlKind.FILTER
+ || aggCall.getKind() == SqlKind.RESPECT_NULLS
+ || aggCall.getKind() == SqlKind.IGNORE_NULLS)) {
+ aggCall = aggCall.operand(0);
+ }
SqlCallBinding opBinding = new SqlCallBinding(validator, scope, aggCall) {
@Override public boolean hasEmptyGroup() {
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 283ea802aa..9bdd3177ff 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -3675,13 +3675,33 @@ void testWinPartClause() {
* Validator rejects FILTER in OVER windows</a>. */
@Test void testOverFilter() {
winSql("SELECT deptno,\n"
- + " ^COUNT(DISTINCT deptno) FILTER (WHERE deptno > 10)^\n"
+ + " ^COUNT(DISTINCT deptno)^ FILTER (WHERE deptno > 10)\n"
+ "OVER win AS agg\n"
+ "FROM emp\n"
- + "WINDOW win AS (PARTITION BY empno)")
+ + "WINDOW win AS (PARTITION BY empno)")
.fails("OVER must be applied to aggregate function");
}
+ /** Test case for <a
href="https://issues.apache.org/jira/browse/CALCITE-7595">[CALCITE-7595]
+ * Support FILTER clause with window functions</a>. */
+ @Test void testFilterWithOver() {
+ winSql("SELECT SUM(sal) FILTER (WHERE sal > 100) OVER (PARTITION BY
deptno) FROM emp")
+ .ok();
+ }
+
+ @Test void testFilterWithOverAndDistinct() {
+ winSql("SELECT SUM(DISTINCT sal) FILTER (WHERE sal > 100) OVER (ORDER BY
deptno) FROM emp")
+ .ok();
+ }
+
+ @Test void testMultipleFiltersWithOver() {
+ winSql("SELECT "
+ + "COUNT(*) FILTER (WHERE empno > 100) OVER (PARTITION BY deptno), "
+ + "SUM(sal) FILTER (WHERE sal > 0) OVER (PARTITION BY deptno) "
+ + "FROM emp")
+ .ok();
+ }
+
@Test void testOverInOrderBy() {
winSql("select sum(deptno) over ^(order by sum(deptno)\n"
+ "over(order by deptno))^ from emp")
diff --git a/core/src/test/resources/sql/winagg.iq
b/core/src/test/resources/sql/winagg.iq
index 6a32b3b3f7..d8348da422 100644
--- a/core/src/test/resources/sql/winagg.iq
+++ b/core/src/test/resources/sql/winagg.iq
@@ -1173,4 +1173,118 @@ order by 1;
(14 rows)
!ok
+
+# [CALCITE-6442] Support FILTER clause with window functions
+# The following 4 tests are related to this issue.
+# Results were validated on Postgres.
+
+# Test 1: FILTER with OVER on COUNT
+select empno, deptno,
+ count(*) filter (where sal > 1500) over (partition by deptno) as
filtered_count
+from emp
+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 |
++-------+--------+----------------+
+(14 rows)
+
+!ok
+
+# Test 2: FILTER with OVER on SUM
+select empno, deptno,
+ sum(sal) filter (where comm is not null) over (partition by deptno) as
filtered_sum
+from emp
+order by empno;
++-------+--------+--------------+
+| EMPNO | DEPTNO | FILTERED_SUM |
++-------+--------+--------------+
+| 7369 | 20 | |
+| 7566 | 20 | |
+| 7788 | 20 | |
+| 7876 | 20 | |
+| 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)
+
+!ok
+
+# Test 3: Multiple FILTER with OVER on different aggregates
+select empno, deptno,
+ count(*) filter (where sal > 1500) over (partition by deptno) as
high_sal_count,
+ sum(sal) filter (where sal <= 1500) over (partition by deptno) as low_sal_sum
+from emp
+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 |
++-------+--------+----------------+-------------+
+(14 rows)
+
+!ok
+
+# Test 4: FILTER with OVER and ORDER BY (running window)
+select empno, deptno, sal,
+ sum(sal) filter (where sal > 1000) over (partition by deptno order by empno
rows between unbounded preceding and current row) as running_sum
+from emp
+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 |
+| 7654 | 30 | 1250.00 | 4100.00 |
+| 7698 | 30 | 2850.00 | 6950.00 |
+| 7844 | 30 | 1500.00 | 8450.00 |
+| 7900 | 30 | 950.00 | |
++-------+--------+---------+-------------+
+(14 rows)
+
+!ok
+
# End winagg.iq
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 454d13456a..80337bbfb1 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2121,9 +2121,11 @@ ### Window functions
windowedAggregateCall:
agg '(' [ ALL | DISTINCT ] value [, value ]* ')'
[ RESPECT NULLS | IGNORE NULLS ]
+ [ FILTER '(' WHERE condition ')' ]
[ WITHIN GROUP '(' ORDER BY orderItem [, orderItem ]* ')' ]
OVER window
| agg '(' '*' ')'
+ [ FILTER '(' WHERE condition ')' ]
OVER window
{% endhighlight %}
@@ -2139,6 +2141,15 @@ ### Window functions
`DISTINCT`, `FILTER` and `WITHIN GROUP` are as described for aggregate
functions.
+#### FILTER clause in window functions
+
+When `FILTER` is used with window functions, it is applied in the following
order:
+
+1. Define window rows by `PARTITION BY` and `ORDER BY`
+2. Apply `ROWS`/`RANGE` bounds to determine the window frame
+3. Apply the `FILTER` condition to rows within that frame
+4. Calculate the aggregate function on the filtered rows
+
| Operator syntax | Description
|:----------------------------------------- |:-----------
| COUNT(value [, value ]*) OVER window | Returns the number of rows in
*window* for which *value* is not null (wholly not null if *value* is composite)