Skip to content

Commit 4f1f00f

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 7020813 commit 4f1f00f

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
@@ -1345,6 +1345,9 @@ public <S> StringBuilder visit(AnalyticExpression analyticExpression, S context)
13451345
builder.append(" ").append(analyticExpression.getWindowName());
13461346
} else if (analyticExpression.getType() != AnalyticType.WITHIN_GROUP_OVER) {
13471347
builder.append(" (");
1348+
if (analyticExpression.getWindowDefinition() != null) {
1349+
analyticExpression.getWindowDefinition().appendBaseWindowNameTo(builder);
1350+
}
13481351

13491352
if (partitionExpressionList != null
13501353
&& !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
@@ -1689,6 +1689,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
16891689
return ":".equals(token.image) || "]".equals(token.image) || ",".equals(token.image);
16901690
}
16911691

1692+
/** Whether a window specification starts with the name of the window it extends. */
1693+
private boolean isBaseWindowNameAhead() {
1694+
Token token = getToken(1);
1695+
switch (token.kind) {
1696+
case K_PARTITION: case K_ORDER: case K_ROWS: case K_RANGE: case K_GROUPS:
1697+
return false;
1698+
default:
1699+
return isIdentifierishNameToken(token);
1700+
}
1701+
}
1702+
16921703
private boolean isAliasAhead() {
16931704
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
16941705
return false;
@@ -8193,6 +8204,9 @@ PlainSelect PlainSelect() #PlainSelect:
81938204
( LOOKAHEAD(2) "," windowName = RelObjectName() <K_AS> winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )*
81948205
{ plainSelect.setWindowDefinitions(winDefs); }
81958206
]
8207+
// DuckDB: QUALIFY after WINDOW
8208+
[ LOOKAHEAD(2, { plainSelect.getQualify() == null }) qualify=Qualify()
8209+
{ plainSelect.setQualify(qualify); plainSelect.setQualifyAfterWindow(true); } ]
81968210
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ]
81978211
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ]
81988212
[ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ]
@@ -13217,8 +13231,12 @@ WindowDefinition windowDefinition() : {
1321713231
WindowElement windowElement = null;
1321813232
boolean partitionByBrackets = false;
1321913233
WindowDefinition winDef = new WindowDefinition();
13234+
String baseWindowName;
1322013235
} {
1322113236
"("
13237+
// DuckDB: (w ORDER BY b) extends the named window w
13238+
[ LOOKAHEAD({ isBaseWindowNameAhead() }) baseWindowName=RelObjectName()
13239+
{ winDef.setBaseWindowName(baseWindowName); } ]
1322213240
[<K_PARTITION> <K_BY>
1322313241
(LOOKAHEAD(3) expressionList=ComplexExpressionList()
1322413242
| "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" )
@@ -13242,7 +13260,7 @@ AnalyticExpression AnalyticExpression(Function function) :
1324213260
{
1324313261
(
1324413262
(
13245-
filter=AggregateFilter() { retval.setType(AnalyticType.FILTER_ONLY); }
13263+
filter=AggregateFilter(retval) { retval.setType(AnalyticType.FILTER_ONLY); }
1324613264
[ LOOKAHEAD(2) windowFun(retval) ]
1324713265
{
1324813266
if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
@@ -13254,19 +13272,29 @@ AnalyticExpression AnalyticExpression(Function function) :
1325413272
)
1325513273
| windowFun(retval)
1325613274
[ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP
13257-
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter() ]
13275+
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter(retval) ]
1325813276
)
1325913277
{
1326013278
retval.setFilterExpression(filter);
1326113279
if (true) { return retval; }
1326213280
}
1326313281
}
1326413282

13265-
/** Shared FILTER predicate for ordinary, window, and ordered-set aggregates. */
13266-
Expression AggregateFilter():
13283+
/**
13284+
* Shared FILTER predicate for ordinary, window, and ordered-set aggregates. DuckDB also accepts
13285+
* it without WHERE, FILTER (x > 1), but PostgreSQL does not.
13286+
*/
13287+
Expression AggregateFilter(AnalyticExpression analyticExpression):
1326713288
{ Expression filter; }
1326813289
{
13269-
<K_FILTER> "(" <K_WHERE> filter=Expression() ")"
13290+
<K_FILTER> "("
13291+
(
13292+
<K_WHERE>
13293+
|
13294+
LOOKAHEAD({ !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
13295+
{ analyticExpression.setFilterWhereKeyword(false); }
13296+
)
13297+
filter=Expression() ")"
1327013298
{ return filter; }
1327113299
}
1327213300

‎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)