Skip to content

Commit 131056c

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 ac8a4f0 commit 131056c

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
@@ -1314,6 +1314,9 @@ public <S> StringBuilder visit(AnalyticExpression analyticExpression, S context)
13141314
builder.append(" ").append(analyticExpression.getWindowName());
13151315
} else if (analyticExpression.getType() != AnalyticType.WITHIN_GROUP_OVER) {
13161316
builder.append(" (");
1317+
if (analyticExpression.getWindowDefinition() != null) {
1318+
analyticExpression.getWindowDefinition().appendBaseWindowNameTo(builder);
1319+
}
13171320

13181321
if (partitionExpressionList != null
13191322
&& !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
@@ -1603,6 +1603,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
16031603
|| next.kind >= MIN_NON_RESERVED_WORD && next.kind <= MAX_NON_RESERVED_WORD;
16041604
}
16051605

1606+
/** Whether a window specification starts with the name of the window it extends. */
1607+
private boolean isBaseWindowNameAhead() {
1608+
Token token = getToken(1);
1609+
switch (token.kind) {
1610+
case K_PARTITION: case K_ORDER: case K_ROWS: case K_RANGE: case K_GROUPS:
1611+
return false;
1612+
default:
1613+
return isIdentifierishNameToken(token);
1614+
}
1615+
}
1616+
16061617
private boolean isAliasAhead() {
16071618
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
16081619
return false;
@@ -8094,6 +8105,9 @@ PlainSelect PlainSelect() #PlainSelect:
80948105
( LOOKAHEAD(2) "," windowName = RelObjectName() <K_AS> winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )*
80958106
{ plainSelect.setWindowDefinitions(winDefs); }
80968107
]
8108+
// DuckDB: QUALIFY after WINDOW
8109+
[ LOOKAHEAD(2, { plainSelect.getQualify() == null }) qualify=Qualify()
8110+
{ plainSelect.setQualify(qualify); plainSelect.setQualifyAfterWindow(true); } ]
80978111
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ]
80988112
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ]
80998113
[ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ]
@@ -13035,8 +13049,12 @@ WindowDefinition windowDefinition() : {
1303513049
WindowElement windowElement = null;
1303613050
boolean partitionByBrackets = false;
1303713051
WindowDefinition winDef = new WindowDefinition();
13052+
String baseWindowName;
1303813053
} {
1303913054
"("
13055+
// DuckDB: (w ORDER BY b) extends the named window w
13056+
[ LOOKAHEAD({ isBaseWindowNameAhead() }) baseWindowName=RelObjectName()
13057+
{ winDef.setBaseWindowName(baseWindowName); } ]
1304013058
[<K_PARTITION> <K_BY>
1304113059
(LOOKAHEAD(3) expressionList=ComplexExpressionList()
1304213060
| "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" )
@@ -13060,7 +13078,7 @@ AnalyticExpression AnalyticExpression(Function function) :
1306013078
{
1306113079
(
1306213080
(
13063-
filter=AggregateFilter() { retval.setType(AnalyticType.FILTER_ONLY); }
13081+
filter=AggregateFilter(retval) { retval.setType(AnalyticType.FILTER_ONLY); }
1306413082
[ LOOKAHEAD(2) windowFun(retval) ]
1306513083
{
1306613084
if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
@@ -13072,19 +13090,29 @@ AnalyticExpression AnalyticExpression(Function function) :
1307213090
)
1307313091
| windowFun(retval)
1307413092
[ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP
13075-
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter() ]
13093+
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter(retval) ]
1307613094
)
1307713095
{
1307813096
retval.setFilterExpression(filter);
1307913097
if (true) { return retval; }
1308013098
}
1308113099
}
1308213100

13083-
/** Shared FILTER predicate for ordinary, window, and ordered-set aggregates. */
13084-
Expression AggregateFilter():
13101+
/**
13102+
* Shared FILTER predicate for ordinary, window, and ordered-set aggregates. DuckDB also accepts
13103+
* it without WHERE, FILTER (x > 1), but PostgreSQL does not.
13104+
*/
13105+
Expression AggregateFilter(AnalyticExpression analyticExpression):
1308513106
{ Expression filter; }
1308613107
{
13087-
<K_FILTER> "(" <K_WHERE> filter=Expression() ")"
13108+
<K_FILTER> "("
13109+
(
13110+
<K_WHERE>
13111+
|
13112+
LOOKAHEAD({ !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
13113+
{ analyticExpression.setFilterWhereKeyword(false); }
13114+
)
13115+
filter=Expression() ")"
1308813116
{ return filter; }
1308913117
}
1309013118

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,4 +865,31 @@ void testTableQueryKeepsTableColumnsAndTrailingClauses() throws JSQLParserExcept
865865
Assertions.assertInstanceOf(net.sf.jsqlparser.statement.piped.FromQuery.class,
866866
((ParenthesedSelect) CCJSqlParserUtil.parse("(FROM t)")).getSelect());
867867
}
868+
869+
@Test
870+
void testWindowAndFilterForms() throws JSQLParserException {
871+
for (String sqlStr : new String[] {
872+
"SELECT a FROM t WINDOW w AS (ORDER BY a) QUALIFY row_number() OVER w = 1",
873+
"SELECT a FROM t QUALIFY row_number() OVER w = 1 WINDOW w AS (ORDER BY a)",
874+
"SELECT sum(a) OVER (w ROWS 2 PRECEDING) FROM t WINDOW w AS (ORDER BY b)",
875+
"SELECT sum(a) OVER (w) FROM t WINDOW w AS (ORDER BY b)",
876+
"SELECT sum(a) OVER w2 FROM t WINDOW w AS (PARTITION BY c), w2 AS (w ORDER BY b)",
877+
"SELECT sum(a) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t",
878+
"SELECT sum(a) FILTER (b > 1) FROM t",
879+
"SELECT count(*) FILTER (b > 1) OVER () FROM t"}) {
880+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
881+
}
882+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
883+
"SELECT sum(a) FILTER (b > 1) OVER (w ORDER BY c) FROM t WINDOW w AS (PARTITION BY d)");
884+
net.sf.jsqlparser.expression.AnalyticExpression sum =
885+
(net.sf.jsqlparser.expression.AnalyticExpression) select.getSelectItem(0)
886+
.getExpression();
887+
Assertions.assertFalse(sum.isFilterWhereKeyword());
888+
// PostgreSQL requires WHERE
889+
Assertions.assertThrows(JSQLParserException.class,
890+
() -> CCJSqlParserUtil.parse("SELECT sum(a) FILTER (b > 1) FROM t",
891+
parser -> parser.withDialect(
892+
net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL)));
893+
Assertions.assertEquals("w", sum.getWindowDefinition().getBaseWindowName());
894+
}
868895
}

0 commit comments

Comments
 (0)