Skip to content

Commit 0db9e0e

Browse files
authored
fix: support FILTER after ordered-set aggregates (#2655)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 4d6bbf3 commit 0db9e0e

4 files changed

Lines changed: 132 additions & 10 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package net.sf.jsqlparser.expression;
1111

12+
import java.util.function.Consumer;
1213
import java.util.Locale;
1314
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
1415
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
@@ -338,10 +339,8 @@ public String toString() {
338339
b.append(keep).append(" ");
339340
}
340341

341-
if (filterExpression != null) {
342-
b.append("FILTER (WHERE ");
343-
b.append(filterExpression);
344-
b.append(")");
342+
if (filterExpression != null && type != AnalyticType.WITHIN_GROUP) {
343+
appendFilterTo(b, b::append);
345344
if (type != AnalyticType.FILTER_ONLY) {
346345
b.append(" ");
347346
}
@@ -382,9 +381,22 @@ public String toString() {
382381
b.append(windowDef.toString());
383382
}
384383

384+
if (filterExpression != null && type == AnalyticType.WITHIN_GROUP) {
385+
b.append(' ');
386+
appendFilterTo(b, b::append);
387+
}
385388
return b.toString();
386389
}
387390

391+
/** Renders the filter using the caller's expression writer. */
392+
public void appendFilterTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
393+
if (filterExpression != null) {
394+
builder.append("FILTER (WHERE ");
395+
expressionPrinter.accept(filterExpression);
396+
builder.append(')');
397+
}
398+
}
399+
388400
public boolean isAllColumns() {
389401
return allColumns;
390402
}

src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,10 +1278,9 @@ public <S> StringBuilder visit(AnalyticExpression analyticExpression, S context)
12781278
builder.append(" ");
12791279
}
12801280

1281-
if (analyticExpression.getFilterExpression() != null) {
1282-
builder.append("FILTER (WHERE ");
1283-
analyticExpression.getFilterExpression().accept(this, context);
1284-
builder.append(")");
1281+
if (analyticExpression.getFilterExpression() != null
1282+
&& analyticExpression.getType() != AnalyticType.WITHIN_GROUP) {
1283+
analyticExpression.appendFilterTo(builder, filter -> filter.accept(this, context));
12851284
if (analyticExpression.getType() != AnalyticType.FILTER_ONLY) {
12861285
builder.append(" ");
12871286
}
@@ -1361,6 +1360,11 @@ public <S> StringBuilder visit(AnalyticExpression analyticExpression, S context)
13611360

13621361
builder.append(")");
13631362
}
1363+
if (analyticExpression.getFilterExpression() != null
1364+
&& analyticExpression.getType() == AnalyticType.WITHIN_GROUP) {
1365+
builder.append(' ');
1366+
analyticExpression.appendFilterTo(builder, filter -> filter.accept(this, context));
1367+
}
13641368
return builder;
13651369
}
13661370

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12345,17 +12345,34 @@ AnalyticExpression AnalyticExpression(Function function) :
1234512345
{
1234612346
(
1234712347
(
12348-
<K_FILTER> "(" <K_WHERE> {retval.setType(AnalyticType.FILTER_ONLY);} filter = Expression() ")"
12348+
filter=AggregateFilter() { retval.setType(AnalyticType.FILTER_ONLY); }
1234912349
[ LOOKAHEAD(2) windowFun(retval) ]
12350+
{
12351+
if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
12352+
&& (retval.getType() == AnalyticType.WITHIN_GROUP
12353+
|| retval.getType() == AnalyticType.WITHIN_GROUP_OVER)) {
12354+
throw new ParseException("FILTER must follow WITHIN GROUP");
12355+
}
12356+
}
1235012357
)
1235112358
| windowFun(retval)
12359+
[ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP
12360+
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter() ]
1235212361
)
1235312362
{
1235412363
retval.setFilterExpression(filter);
12355-
return retval;
12364+
if (true) { return retval; }
1235612365
}
1235712366
}
1235812367

12368+
/** Shared FILTER predicate for ordinary, window, and ordered-set aggregates. */
12369+
Expression AggregateFilter():
12370+
{ Expression filter; }
12371+
{
12372+
<K_FILTER> "(" <K_WHERE> filter=Expression() ")"
12373+
{ return filter; }
12374+
}
12375+
1235912376
WindowElement WindowElement():
1236012377
{
1236112378
WindowElement windowElement = new WindowElement();
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.statement.select.PlainSelect;
20+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
21+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
22+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
class OrderedSetFilterTest {
28+
@ParameterizedTest
29+
@ValueSource(strings = {
30+
"percentile_cont(0.5) WITHIN GROUP (ORDER BY score) FILTER (WHERE active)",
31+
"percentile_disc(0.5) WITHIN GROUP (ORDER BY score DESC NULLS LAST) "
32+
+ "FILTER (WHERE active AND score > 0)",
33+
"mode() WITHIN GROUP (ORDER BY score) FILTER (WHERE active)",
34+
"rank(5) WITHIN GROUP (ORDER BY score) FILTER (WHERE NOT active)",
35+
"sum(score) FILTER (WHERE active)",
36+
"sum(score) FILTER (WHERE active) OVER (PARTITION BY team)",
37+
"percentile_cont(0.5) WITHIN GROUP (ORDER BY score)"
38+
})
39+
void preservesAggregateClauses(String expression) throws JSQLParserException {
40+
String sql = "SELECT " + expression + " FROM measurements";
41+
PlainSelect select = parse(sql);
42+
StringBuilder output = new StringBuilder();
43+
select.accept(new StatementDeParser(output), null);
44+
assertEquals(sql, select.toString().replace(" )", ")"));
45+
assertEquals(sql, output.toString().replace(" )", ")"));
46+
assertEquals(select.toString(), parse(output.toString()).toString());
47+
assertEquals(select.toString(), CCJSqlParserUtil.parse(sql).toString());
48+
}
49+
50+
@Test
51+
void visitsBothOrderingAndFilterExpressions() throws JSQLParserException {
52+
PlainSelect select = parse("SELECT rank(5) WITHIN GROUP (ORDER BY score + 1) "
53+
+ "FILTER (WHERE score > 2) FROM measurements");
54+
AnalyticExpression aggregate = assertInstanceOf(AnalyticExpression.class,
55+
select.getSelectItem(0).getExpression());
56+
assertEquals(AnalyticType.WITHIN_GROUP, aggregate.getType());
57+
assertEquals("score > 2", aggregate.getFilterExpression().toString());
58+
StringBuilder output = new StringBuilder();
59+
ExpressionDeParser expressions = new ExpressionDeParser() {
60+
@Override
61+
public <S> StringBuilder visit(LongValue value, S context) {
62+
return getBuilder().append(value.getValue() + 10);
63+
}
64+
};
65+
select.accept(new StatementDeParser(expressions, new SelectDeParser(), output), null);
66+
String expected = "SELECT rank(15) WITHIN GROUP (ORDER BY score + 11) "
67+
+ "FILTER (WHERE score > 12) FROM measurements";
68+
assertEquals(expected, output.toString());
69+
assertEquals(expected, parse(output.toString()).toString());
70+
}
71+
72+
@ParameterizedTest
73+
@ValueSource(strings = {
74+
"percentile_cont(0.5) FILTER (WHERE active) WITHIN GROUP (ORDER BY score)",
75+
"percentile_cont(0.5) WITHIN GROUP (ORDER BY score) FILTER (active)",
76+
"percentile_cont(0.5) WITHIN GROUP (ORDER BY score) FILTER (WHERE)",
77+
"percentile_cont(0.5) WITHIN GROUP (ORDER BY score) "
78+
+ "FILTER (WHERE active) FILTER (WHERE active)",
79+
"sum(score) OVER (PARTITION BY team) FILTER (WHERE active)"
80+
})
81+
void rejectsInvalidPostgresFilterPlacement(String expression) {
82+
assertThrows(JSQLParserException.class,
83+
() -> parse("SELECT " + expression + " FROM measurements"));
84+
}
85+
86+
private static PlainSelect parse(String sql) throws JSQLParserException {
87+
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
88+
}
89+
}

0 commit comments

Comments
 (0)