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 fcb07463d0 [CALCITE-7485] FIRST_VALUE/LAST_VALUE should only be
defined for window aggregates
fcb07463d0 is described below
commit fcb07463d05ac072f86634fdb53c70b113bb1b75
Author: Yu Xu <[email protected]>
AuthorDate: Wed May 6 14:33:10 2026 +0800
[CALCITE-7485] FIRST_VALUE/LAST_VALUE should only be defined for window
aggregates
---
.../org/apache/calcite/sql/SqlAggFunction.java | 5 +++
.../apache/calcite/sql/validate/SqlValidator.java | 6 ++++
.../calcite/sql/validate/SqlValidatorImpl.java | 5 ++-
.../org/apache/calcite/test/RelOptRulesTest.java | 3 +-
.../org/apache/calcite/test/SqlValidatorTest.java | 36 +++++++++++++++++++++-
.../org/apache/calcite/test/RelOptRulesTest.xml | 9 +++---
.../org/apache/calcite/test/SqlOperatorTest.java | 28 +++++++++++------
7 files changed, 73 insertions(+), 19 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlAggFunction.java
b/core/src/main/java/org/apache/calcite/sql/SqlAggFunction.java
index 59cb7522e6..7ba19c48de 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlAggFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlAggFunction.java
@@ -27,6 +27,7 @@
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Optionality;
+import org.apache.calcite.util.Static;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -135,6 +136,10 @@ protected SqlAggFunction(
SqlValidator validator,
SqlValidatorScope scope,
SqlValidatorScope operandScope) {
+ if (requiresOver() && !validator.isInWindow()) {
+ throw validator.newValidationError(call,
+ Static.RESOURCE.absentOverClause());
+ }
super.validateCall(call, validator, scope, operandScope);
validator.validateAggregateParams(call, null, null, null, scope);
}
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
index b113dec80f..3846ed5daf 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
@@ -295,6 +295,12 @@ void validateWindow(
SqlValidatorScope scope,
@Nullable SqlCall call);
+ /** Returns whether the validator is currently validating within a window
+ * expression. */
+ default boolean isInWindow() {
+ return false;
+ }
+
/**
* Validates a MATCH_RECOGNIZE clause.
*
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 6881bae035..63f20d5941 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -6210,6 +6210,10 @@ public void setOriginal(SqlNode expr, SqlNode original) {
return new FieldNamespace(this, field.getType());
}
+ @Override public boolean isInWindow() {
+ return inWindow;
+ }
+
@Override public void validateWindow(
SqlNode windowOrId,
SqlValidatorScope scope,
@@ -6832,7 +6836,6 @@ private SqlNode navigationInDefine(SqlNode node, String
alpha) {
break;
case 2:
assert op.allowsNullTreatment();
- assert op.requiresOver();
assert op.requiresGroupOrder() == Optionality.FORBIDDEN;
break;
default:
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index e527029749..0f5959fb57 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -10067,8 +10067,7 @@ private RelOptFixture spatial(String sql) {
*/
@Test void testReduceAggregateFunctionsByGroup() {
final String sql = "select sal, max(sal) as sal_max, min(sal) as
sal_min,\n"
- + "avg(sal) sal_avg, any_value(sal) as sal_val, first_value(sal) as
sal_first,\n"
- + "last_value(sal) as sal_last\n"
+ + "avg(sal) sal_avg, any_value(sal) as sal_val\n"
+ "from emp group by sal, deptno";
sql(sql).withRule(CoreRules.AGGREGATE_REDUCE_FUNCTIONS,
CoreRules.PROJECT_MERGE).check();
}
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 20602e8d75..36ef279b53 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -3374,7 +3374,10 @@ void testWinPartClause() {
* <a href="https://issues.apache.org/jira/browse/CALCITE-820">[CALCITE-820]
* Validate that window functions have OVER clause</a>, and
* <a
href="https://issues.apache.org/jira/browse/CALCITE-1340">[CALCITE-1340]
- * Window aggregates give invalid errors</a>. */
+ * Window aggregates give invalid errors</a> and
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7485">[CALCITE-7485]
+ * FIRST_VALUE/LAST_VALUE should only be defined for window aggregates</a>.
+ * */
@Test void testWindowFunctionsWithoutOver() {
winSql("select sum(empno)\n"
+ "from emp\n"
@@ -3397,6 +3400,37 @@ void testWinPartClause() {
winSql("select ^nth_value(sal, 2)^\n"
+ "from emp")
.fails("OVER clause is necessary for window functions");
+
+ winSql("select ^first_value(sal)^\n"
+ + "from emp")
+ .fails("OVER clause is necessary for window functions");
+
+ winSql("select ^last_value(sal)^\n"
+ + "from emp")
+ .fails("OVER clause is necessary for window functions");
+
+ // With alias, first_value and last_value without OVER should also fail
+ winSql("select ^first_value(sal)^ as sal_first\n"
+ + "from emp")
+ .fails("OVER clause is necessary for window functions");
+
+ winSql("select ^last_value(sal)^ as sal_last\n"
+ + "from emp")
+ .fails("OVER clause is necessary for window functions");
+
+ // In GROUP BY context, first_value and last_value without OVER should fail
+ winSql("select sal, ^first_value(sal)^ as sal_first\n"
+ + "from emp group by sal, deptno")
+ .fails("OVER clause is necessary for window functions");
+
+ // first_value and last_value with OVER clause should succeed
+ winSql("select first_value(sal) over (order by empno)\n"
+ + "from emp")
+ .ok();
+
+ winSql("select last_value(sal) over (order by empno)\n"
+ + "from emp")
+ .ok();
}
@Test void testOverInPartitionBy() {
diff --git
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index cf66384c12..fc60b88649 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -15968,21 +15968,20 @@ LogicalProject(EXPR$0=[1], A=[$1])
<TestCase name="testReduceAggregateFunctionsByGroup">
<Resource name="sql">
<![CDATA[select sal, max(sal) as sal_max, min(sal) as sal_min,
-avg(sal) sal_avg, any_value(sal) as sal_val, first_value(sal) as sal_first,
-last_value(sal) as sal_last
+avg(sal) sal_avg, any_value(sal) as sal_val
from emp group by sal, deptno]]>
</Resource>
<Resource name="planBefore">
<![CDATA[
-LogicalProject(SAL=[$0], SAL_MAX=[$2], SAL_MIN=[$3], SAL_AVG=[$4],
SAL_VAL=[$5], SAL_FIRST=[$6], SAL_LAST=[$7])
- LogicalAggregate(group=[{0, 1}], SAL_MAX=[MAX($0)], SAL_MIN=[MIN($0)],
SAL_AVG=[AVG($0)], SAL_VAL=[ANY_VALUE($0)], SAL_FIRST=[FIRST_VALUE($0)],
SAL_LAST=[LAST_VALUE($0)])
+LogicalProject(SAL=[$0], SAL_MAX=[$2], SAL_MIN=[$3], SAL_AVG=[$4],
SAL_VAL=[$5])
+ LogicalAggregate(group=[{0, 1}], SAL_MAX=[MAX($0)], SAL_MIN=[MIN($0)],
SAL_AVG=[AVG($0)], SAL_VAL=[ANY_VALUE($0)])
LogicalProject(SAL=[$5], DEPTNO=[$7])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
<Resource name="planAfter">
<![CDATA[
-LogicalProject(SAL=[$0], SAL_MAX=[$0], SAL_MIN=[$0], SAL_AVG=[$0],
SAL_VAL=[$0], SAL_FIRST=[$0], SAL_LAST=[$0])
+LogicalProject(SAL=[$0], SAL_MAX=[$0], SAL_MIN=[$0], SAL_AVG=[$0],
SAL_VAL=[$0])
LogicalAggregate(group=[{0, 1}])
LogicalProject(SAL=[$5], DEPTNO=[$7])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index a6d2eb6f0d..cf2792c5a6 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -15836,30 +15836,38 @@ void testTimestampDiff(boolean coercionEnabled) {
+ "between 0 and 1", false);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7485">[CALCITE-7485]
+ * FIRST_VALUE/LAST_VALUE should only be defined for window aggregates</a>.
+ */
@Test void testPercentileContBigQueryFunc() {
final SqlOperatorFixture f = fixture()
.setFor(SqlLibraryOperators.PERCENTILE_CONT2,
SqlOperatorFixture.VmName.EXPAND)
.withLibrary(SqlLibrary.BIG_QUERY);
- f.checkType("percentile_cont(1, .5)",
+ f.checkType("percentile_cont(1, .5) over ()",
"DOUBLE NOT NULL");
- f.checkType("percentile_cont(.5, .5 RESPECT NULLS)", "DOUBLE NOT NULL");
- f.checkType("percentile_cont(1, .5 IGNORE NULLS)", "DOUBLE NOT NULL");
- f.checkType("percentile_cont(2+3, .5 IGNORE NULLS)", "DOUBLE NOT NULL");
- f.checkFails("^percentile_cont(1, 1.5)^",
+ f.checkType("percentile_cont(.5, .5 RESPECT NULLS) over ()", "DOUBLE NOT
NULL");
+ f.checkType("percentile_cont(1, .5 IGNORE NULLS) over ()", "DOUBLE NOT
NULL");
+ f.checkType("percentile_cont(2+3, .5 IGNORE NULLS) over ()", "DOUBLE NOT
NULL");
+ f.checkFails("^percentile_cont(1, 1.5)^ over ()",
"Argument to function 'PERCENTILE_CONT' must be a numeric literal "
+ "between 0 and 1", false);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7485">[CALCITE-7485]
+ * FIRST_VALUE/LAST_VALUE should only be defined for window aggregates</a>.
+ */
@Test void testPercentileDiscBigQueryFunc() {
final SqlOperatorFixture f = fixture()
.setFor(SqlLibraryOperators.PERCENTILE_DISC2,
SqlOperatorFixture.VmName.EXPAND)
.withLibrary(SqlLibrary.BIG_QUERY);
- f.checkType("percentile_disc(1, .5)",
+ f.checkType("percentile_disc(1, .5) over ()",
"INTEGER NOT NULL");
- f.checkType("percentile_disc(1, .5 RESPECT NULLS)", "INTEGER NOT NULL");
- f.checkType("percentile_disc(0.75, .5 IGNORE NULLS)", "DECIMAL(3, 2) NOT
NULL");
- f.checkType("percentile_disc(2+3, .5 IGNORE NULLS)", "INTEGER NOT NULL");
- f.checkFails("^percentile_disc(1, 1.5)^",
+ f.checkType("percentile_disc(1, .5 RESPECT NULLS) over ()", "INTEGER NOT
NULL");
+ f.checkType("percentile_disc(0.75, .5 IGNORE NULLS) over ()", "DECIMAL(3,
2) NOT NULL");
+ f.checkType("percentile_disc(2+3, .5 IGNORE NULLS) over ()", "INTEGER NOT
NULL");
+ f.checkFails("^percentile_disc(1, 1.5)^ over ()",
"Argument to function 'PERCENTILE_DISC' must be a numeric literal "
+ "between 0 and 1", false);
}