Skip to content

Commit ed3fb3a

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 f7d84c8 commit ed3fb3a

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

13501353
if (partitionExpressionList != null
13511354
&& !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
@@ -1833,6 +1833,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
18331833
}
18341834
}
18351835

1836+
/** Whether a window specification starts with the name of the window it extends. */
1837+
private boolean isBaseWindowNameAhead() {
1838+
Token token = getToken(1);
1839+
switch (token.kind) {
1840+
case K_PARTITION: case K_ORDER: case K_ROWS: case K_RANGE: case K_GROUPS:
1841+
return false;
1842+
default:
1843+
return isIdentifierishNameToken(token);
1844+
}
1845+
}
1846+
18361847
private boolean isAliasAhead() {
18371848
if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) {
18381849
return false;
@@ -8409,6 +8420,9 @@ PlainSelect PlainSelect() #PlainSelect:
84098420
( LOOKAHEAD(2) "," windowName = RelObjectName() <K_AS> winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )*
84108421
{ plainSelect.setWindowDefinitions(winDefs); }
84118422
]
8423+
// DuckDB: QUALIFY after WINDOW
8424+
[ LOOKAHEAD(2, { plainSelect.getQualify() == null }) qualify=Qualify()
8425+
{ plainSelect.setQualify(qualify); plainSelect.setQualifyAfterWindow(true); } ]
84128426
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ]
84138427
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ]
84148428
[ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ]
@@ -13498,8 +13512,12 @@ WindowDefinition windowDefinition() : {
1349813512
WindowElement windowElement = null;
1349913513
boolean partitionByBrackets = false;
1350013514
WindowDefinition winDef = new WindowDefinition();
13515+
String baseWindowName;
1350113516
} {
1350213517
"("
13518+
// DuckDB: (w ORDER BY b) extends the named window w
13519+
[ LOOKAHEAD({ isBaseWindowNameAhead() }) baseWindowName=RelObjectName()
13520+
{ winDef.setBaseWindowName(baseWindowName); } ]
1350313521
[<K_PARTITION> <K_BY>
1350413522
(LOOKAHEAD(3) expressionList=ComplexExpressionList()
1350513523
| "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" )
@@ -13523,7 +13541,7 @@ AnalyticExpression AnalyticExpression(Function function) :
1352313541
{
1352413542
(
1352513543
(
13526-
filter=AggregateFilter() { retval.setType(AnalyticType.FILTER_ONLY); }
13544+
filter=AggregateFilter(retval) { retval.setType(AnalyticType.FILTER_ONLY); }
1352713545
[ LOOKAHEAD(2) windowFun(retval) ]
1352813546
{
1352913547
if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
@@ -13535,19 +13553,29 @@ AnalyticExpression AnalyticExpression(Function function) :
1353513553
)
1353613554
| windowFun(retval)
1353713555
[ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP
13538-
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter() ]
13556+
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter(retval) ]
1353913557
)
1354013558
{
1354113559
retval.setFilterExpression(filter);
1354213560
if (true) { return retval; }
1354313561
}
1354413562
}
1354513563

13546-
/** Shared FILTER predicate for ordinary, window, and ordered-set aggregates. */
13547-
Expression AggregateFilter():
13564+
/**
13565+
* Shared FILTER predicate for ordinary, window, and ordered-set aggregates. DuckDB also accepts
13566+
* it without WHERE, FILTER (x > 1), but PostgreSQL does not.
13567+
*/
13568+
Expression AggregateFilter(AnalyticExpression analyticExpression):
1354813569
{ Expression filter; }
1354913570
{
13550-
<K_FILTER> "(" <K_WHERE> filter=Expression() ")"
13571+
<K_FILTER> "("
13572+
(
13573+
<K_WHERE>
13574+
|
13575+
LOOKAHEAD({ !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
13576+
{ analyticExpression.setFilterWhereKeyword(false); }
13577+
)
13578+
filter=Expression() ")"
1355113579
{ return filter; }
1355213580
}
1355313581

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,4 +1072,31 @@ void testTrailingCommaInSelectList() throws JSQLParserException {
10721072
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a, " + item + " FROM t", true);
10731073
}
10741074
}
1075+
1076+
@Test
1077+
void testWindowAndFilterForms() throws JSQLParserException {
1078+
for (String sqlStr : new String[] {
1079+
"SELECT a FROM t WINDOW w AS (ORDER BY a) QUALIFY row_number() OVER w = 1",
1080+
"SELECT a FROM t QUALIFY row_number() OVER w = 1 WINDOW w AS (ORDER BY a)",
1081+
"SELECT sum(a) OVER (w ROWS 2 PRECEDING) FROM t WINDOW w AS (ORDER BY b)",
1082+
"SELECT sum(a) OVER (w) FROM t WINDOW w AS (ORDER BY b)",
1083+
"SELECT sum(a) OVER w2 FROM t WINDOW w AS (PARTITION BY c), w2 AS (w ORDER BY b)",
1084+
"SELECT sum(a) OVER (ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t",
1085+
"SELECT sum(a) FILTER (b > 1) FROM t",
1086+
"SELECT count(*) FILTER (b > 1) OVER () FROM t"}) {
1087+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
1088+
}
1089+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
1090+
"SELECT sum(a) FILTER (b > 1) OVER (w ORDER BY c) FROM t WINDOW w AS (PARTITION BY d)");
1091+
net.sf.jsqlparser.expression.AnalyticExpression sum =
1092+
(net.sf.jsqlparser.expression.AnalyticExpression) select.getSelectItem(0)
1093+
.getExpression();
1094+
Assertions.assertFalse(sum.isFilterWhereKeyword());
1095+
// PostgreSQL requires WHERE
1096+
Assertions.assertThrows(JSQLParserException.class,
1097+
() -> CCJSqlParserUtil.parse("SELECT sum(a) FILTER (b > 1) FROM t",
1098+
parser -> parser.withDialect(
1099+
net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL)));
1100+
Assertions.assertEquals("w", sum.getWindowDefinition().getBaseWindowName());
1101+
}
10751102
}

0 commit comments

Comments
 (0)