Skip to content

Commit 67f8955

Browse files
committed
DuckDB: support QUALIFY after WINDOW, OVER (w ...) and FILTER without WHERE
- QUALIFY may follow the WINDOW clause and prints back there (`PlainSelect.isQualifyAfterWindow()`). - A window specification may start with the name of the window it extends, `OVER (w ORDER BY b)`, also in WINDOW definitions (`WindowDefinition.getBaseWindowName()`). - `FILTER (b > 1)` without WHERE is accepted except under the PostgreSQL dialect (`AnalyticExpression.isFilterWhereKeyword()`).
1 parent fde4f03 commit 67f8955

7 files changed

Lines changed: 122 additions & 8 deletions

File tree

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class AnalyticExpression extends ASTNodeAccessImpl implements Expression
4141
private boolean unique = false;
4242
private boolean ignoreNullsOutside = false; // IGNORE NULLS outside function parameters
4343
private Expression filterExpression = null;
44+
private boolean filterWhereKeyword = true;
4445
private List<OrderByElement> funcOrderBy = null;
4546
private String onOverflowTruncate = null;
4647

@@ -391,12 +392,22 @@ public String toString() {
391392
/** Renders the filter using the caller's expression writer. */
392393
public void appendFilterTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
393394
if (filterExpression != null) {
394-
builder.append("FILTER (WHERE ");
395+
builder.append(filterWhereKeyword ? "FILTER (WHERE " : "FILTER (");
395396
expressionPrinter.accept(filterExpression);
396397
builder.append(')');
397398
}
398399
}
399400

401+
/** Whether the FILTER clause has WHERE, which DuckDB allows to leave out. */
402+
public boolean isFilterWhereKeyword() {
403+
return filterWhereKeyword;
404+
}
405+
406+
public AnalyticExpression setFilterWhereKeyword(boolean filterWhereKeyword) {
407+
this.filterWhereKeyword = filterWhereKeyword;
408+
return this;
409+
}
410+
400411
public boolean isAllColumns() {
401412
return allColumns;
402413
}

‎src/main/java/net/sf/jsqlparser/expression/WindowDefinition.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class WindowDefinition implements Serializable {
2323
final PartitionByClause partitionBy = new PartitionByClause();
2424
WindowElement windowElement = null;
2525
private String windowName;
26+
private String baseWindowName;
2627

2728
public OrderByClause getOrderBy() {
2829
return orderBy;
@@ -61,6 +62,31 @@ public void setPartitionExpressionList(ExpressionList<Expression> partitionExpre
6162
partitionBy.setExpressions(partitionExpressionList, brackets);
6263
}
6364

65+
/** DuckDB: the window this one extends, as w in OVER (w ORDER BY b), or {@code null}. */
66+
public String getBaseWindowName() {
67+
return baseWindowName;
68+
}
69+
70+
public WindowDefinition setBaseWindowName(String baseWindowName) {
71+
this.baseWindowName = baseWindowName;
72+
return this;
73+
}
74+
75+
/** Appends the base window name, separated from any following part. */
76+
public void appendBaseWindowNameTo(StringBuilder builder) {
77+
if (baseWindowName != null) {
78+
builder.append(baseWindowName);
79+
boolean more = partitionBy.getPartitionExpressionList() != null
80+
&& !partitionBy.getPartitionExpressionList().isEmpty()
81+
|| orderBy.getOrderByElements() != null
82+
&& !orderBy.getOrderByElements().isEmpty()
83+
|| windowElement != null;
84+
if (more) {
85+
builder.append(' ');
86+
}
87+
}
88+
}
89+
6490
public String getWindowName() {
6591
return windowName;
6692
}
@@ -105,6 +131,7 @@ public String toString() {
105131
b.append(windowName).append(" AS ");
106132
}
107133
b.append("(");
134+
appendBaseWindowNameTo(b);
108135
partitionBy.toStringPartitionBy(b);
109136
orderBy.toStringOrderByElements(b);
110137

‎src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class PlainSelect extends Select {
4545
private GroupByElement groupBy;
4646
private Expression having;
4747
private Expression qualify;
48+
private boolean qualifyAfterWindow;
4849
private OptimizeFor optimizeFor;
4950
private Skip skip;
5051
private final EnumSet<MySqlSelectModifier> mySqlSelectModifiers =
@@ -442,6 +443,16 @@ public Expression getQualify() {
442443
return qualify;
443444
}
444445

446+
/** Whether QUALIFY follows the WINDOW clause, as in DuckDB, rather than preceding it. */
447+
public boolean isQualifyAfterWindow() {
448+
return qualifyAfterWindow;
449+
}
450+
451+
public PlainSelect setQualifyAfterWindow(boolean qualifyAfterWindow) {
452+
this.qualifyAfterWindow = qualifyAfterWindow;
453+
return this;
454+
}
455+
445456
public PlainSelect setQualify(Expression qualify) {
446457
this.qualify = qualify;
447458
return this;
@@ -595,14 +606,17 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
595606
if (having != null) {
596607
builder.append(" HAVING ").append(having);
597608
}
598-
if (qualify != null) {
609+
if (qualify != null && !qualifyAfterWindow) {
599610
builder.append(" QUALIFY ").append(qualify);
600611
}
601612
if (windowDefinitions != null) {
602613
builder.append(" WINDOW ");
603614
builder.append(windowDefinitions.stream().map(WindowDefinition::toString)
604615
.collect(joining(", ")));
605616
}
617+
if (qualify != null && qualifyAfterWindow) {
618+
builder.append(" QUALIFY ").append(qualify);
619+
}
606620
appendEmitClauseTo(builder);
607621
if (intoTempTable != null) {
608622
builder.append(" INTO TEMP ").append(intoTempTable);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,9 @@ public <S> StringBuilder visit(AnalyticExpression analyticExpression, S context)
13301330
builder.append(" ").append(analyticExpression.getWindowName());
13311331
} else if (analyticExpression.getType() != AnalyticType.WITHIN_GROUP_OVER) {
13321332
builder.append(" (");
1333+
if (analyticExpression.getWindowDefinition() != null) {
1334+
analyticExpression.getWindowDefinition().appendBaseWindowNameTo(builder);
1335+
}
13331336

13341337
if (partitionExpressionList != null
13351338
&& !partitionExpressionList.isEmpty()) {

‎src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
233233
builder.append(" HAVING ");
234234
plainSelect.getHaving().accept(expressionVisitor, context);
235235
}
236-
if (plainSelect.getQualify() != null) {
236+
if (plainSelect.getQualify() != null && !plainSelect.isQualifyAfterWindow()) {
237237
builder.append(" QUALIFY ");
238238
plainSelect.getQualify().accept(expressionVisitor, context);
239239
}
@@ -242,6 +242,10 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
242242
builder.append(plainSelect.getWindowDefinitions().stream()
243243
.map(WindowDefinition::toString).collect(joining(", ")));
244244
}
245+
if (plainSelect.getQualify() != null && plainSelect.isQualifyAfterWindow()) {
246+
builder.append(" QUALIFY ");
247+
plainSelect.getQualify().accept(expressionVisitor, context);
248+
}
245249
Alias alias = plainSelect.getAlias();
246250
if (alias != null) {
247251
builder.append(alias);

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
16591659
|| next.kind >= MIN_NON_RESERVED_WORD && next.kind <= MAX_NON_RESERVED_WORD;
16601660
}
16611661

1662+
/** Whether a window specification starts with the name of the window it extends. */
1663+
private boolean isBaseWindowNameAhead() {
1664+
Token token = getToken(1);
1665+
switch (token.kind) {
1666+
case K_PARTITION: case K_ORDER: case K_ROWS: case K_RANGE: case K_GROUPS:
1667+
return false;
1668+
default:
1669+
return isIdentifierishNameToken(token);
1670+
}
1671+
}
1672+
16621673
private boolean isAliasAhead() {
16631674
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
16641675
return false;
@@ -8157,6 +8168,9 @@ PlainSelect PlainSelect() #PlainSelect:
81578168
( LOOKAHEAD(2) "," windowName = RelObjectName() <K_AS> winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )*
81588169
{ plainSelect.setWindowDefinitions(winDefs); }
81598170
]
8171+
// DuckDB: QUALIFY after WINDOW
8172+
[ LOOKAHEAD(2, { plainSelect.getQualify() == null }) qualify=Qualify()
8173+
{ plainSelect.setQualify(qualify); plainSelect.setQualifyAfterWindow(true); } ]
81608174
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ]
81618175
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ]
81628176
[ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ]
@@ -13125,8 +13139,12 @@ WindowDefinition windowDefinition() : {
1312513139
WindowElement windowElement = null;
1312613140
boolean partitionByBrackets = false;
1312713141
WindowDefinition winDef = new WindowDefinition();
13142+
String baseWindowName;
1312813143
} {
1312913144
"("
13145+
// DuckDB: (w ORDER BY b) extends the named window w
13146+
[ LOOKAHEAD({ isBaseWindowNameAhead() }) baseWindowName=RelObjectName()
13147+
{ winDef.setBaseWindowName(baseWindowName); } ]
1313013148
[<K_PARTITION> <K_BY>
1313113149
(LOOKAHEAD(3) expressionList=ComplexExpressionList()
1313213150
| "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" )
@@ -13150,7 +13168,7 @@ AnalyticExpression AnalyticExpression(Function function) :
1315013168
{
1315113169
(
1315213170
(
13153-
filter=AggregateFilter() { retval.setType(AnalyticType.FILTER_ONLY); }
13171+
filter=AggregateFilter(retval) { retval.setType(AnalyticType.FILTER_ONLY); }
1315413172
[ LOOKAHEAD(2) windowFun(retval) ]
1315513173
{
1315613174
if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
@@ -13162,19 +13180,29 @@ AnalyticExpression AnalyticExpression(Function function) :
1316213180
)
1316313181
| windowFun(retval)
1316413182
[ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP
13165-
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter() ]
13183+
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter(retval) ]
1316613184
)
1316713185
{
1316813186
retval.setFilterExpression(filter);
1316913187
if (true) { return retval; }
1317013188
}
1317113189
}
1317213190

13173-
/** Shared FILTER predicate for ordinary, window, and ordered-set aggregates. */
13174-
Expression AggregateFilter():
13191+
/**
13192+
* Shared FILTER predicate for ordinary, window, and ordered-set aggregates. DuckDB also accepts
13193+
* it without WHERE, FILTER (x > 1), but PostgreSQL does not.
13194+
*/
13195+
Expression AggregateFilter(AnalyticExpression analyticExpression):
1317513196
{ Expression filter; }
1317613197
{
13177-
<K_FILTER> "(" <K_WHERE> filter=Expression() ")"
13198+
<K_FILTER> "("
13199+
(
13200+
<K_WHERE>
13201+
|
13202+
LOOKAHEAD({ !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
13203+
{ analyticExpression.setFilterWhereKeyword(false); }
13204+
)
13205+
filter=Expression() ")"
1317813206
{ return filter; }
1317913207
}
1318013208

‎src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,4 +986,31 @@ private static String tree(net.sf.jsqlparser.expression.Expression expression) {
986986
}
987987
return expression.toString();
988988
}
989+
990+
@Test
991+
void testWindowAndFilterForms() throws JSQLParserException {
992+
for (String sqlStr : new String[] {
993+
"SELECT a FROM t WINDOW w AS (ORDER BY a) QUALIFY row_number() OVER w = 1",
994+
"SELECT a FROM t QUALIFY row_number() OVER w = 1 WINDOW w AS (ORDER BY a)",
995+
"SELECT sum(a) OVER (w ROWS 2 PRECEDING) FROM t WINDOW w AS (ORDER BY b)",
996+
"SELECT sum(a) OVER (w) FROM t WINDOW w AS (ORDER BY b)",
997+
"SELECT sum(a) OVER w2 FROM t WINDOW w AS (PARTITION BY c), w2 AS (w ORDER BY b)",
998+
"SELECT sum(a) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t",
999+
"SELECT sum(a) FILTER (b > 1) FROM t",
1000+
"SELECT count(*) FILTER (b > 1) OVER () FROM t"}) {
1001+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
1002+
}
1003+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
1004+
"SELECT sum(a) FILTER (b > 1) OVER (w ORDER BY c) FROM t WINDOW w AS (PARTITION BY d)");
1005+
net.sf.jsqlparser.expression.AnalyticExpression sum =
1006+
(net.sf.jsqlparser.expression.AnalyticExpression) select.getSelectItem(0)
1007+
.getExpression();
1008+
Assertions.assertFalse(sum.isFilterWhereKeyword());
1009+
// PostgreSQL requires WHERE
1010+
Assertions.assertThrows(JSQLParserException.class,
1011+
() -> CCJSqlParserUtil.parse("SELECT sum(a) FILTER (b > 1) FROM t",
1012+
parser -> parser.withDialect(
1013+
net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL)));
1014+
Assertions.assertEquals("w", sum.getWindowDefinition().getBaseWindowName());
1015+
}
9891016
}

0 commit comments

Comments
 (0)