Skip to content

Commit 7dcdeec

Browse files
authored
DuckDB: support QUALIFY after WINDOW, OVER (w ...) and FILTER without WHERE (#2803)
* 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()`). * Keep window specifications parseable inside lookahead - The base window name is a choice rather than an optional, so that lookahead scans through a window specification keep its check: `JSON_VALUE(... DEFAULT sum(a) OVER (PARTITION BY b) ON EMPTY)` parses again. - Only an OVER window may extend a named window, not WITHIN GROUP. - QUALIFY after WINDOW requires a WINDOW clause.
1 parent 8f0bd0c commit 7dcdeec

7 files changed

Lines changed: 141 additions & 9 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: 45 additions & 6 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;
@@ -8408,6 +8419,9 @@ PlainSelect PlainSelect() #PlainSelect:
84088419
windowName = RelObjectName() <K_AS> winDef = windowDefinition() { List<WindowDefinition> winDefs = new ArrayList<WindowDefinition>(); winDefs.add(winDef.withWindowName(windowName)); }
84098420
( LOOKAHEAD(2) "," windowName = RelObjectName() <K_AS> winDef = windowDefinition() { winDefs.add(winDef.withWindowName(windowName)); } )*
84108421
{ plainSelect.setWindowDefinitions(winDefs); }
8422+
// DuckDB: QUALIFY after WINDOW
8423+
[ LOOKAHEAD(2, { plainSelect.getQualify() == null }) qualify=Qualify()
8424+
{ plainSelect.setQualify(qualify); plainSelect.setQualifyAfterWindow(true); } ]
84118425
]
84128426
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ]
84138427
[ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ]
@@ -13479,7 +13493,13 @@ void windowFun(AnalyticExpression retval):{
1347913493
(
1348013494
windowName = RelObjectName() { retval.setWindowName(windowName); }
1348113495
|
13482-
winDef = windowDefinition() { retval.setWindowDefinition(winDef); }
13496+
winDef = windowDefinition()
13497+
{
13498+
if (winDef.getBaseWindowName() != null && retval.getType() != AnalyticType.OVER) {
13499+
throw new ParseException("Only an OVER window may extend a named window");
13500+
}
13501+
retval.setWindowDefinition(winDef);
13502+
}
1348313503

1348413504
[
1348513505
LOOKAHEAD(2) <K_OVER> "("
@@ -13498,8 +13518,17 @@ WindowDefinition windowDefinition() : {
1349813518
WindowElement windowElement = null;
1349913519
boolean partitionByBrackets = false;
1350013520
WindowDefinition winDef = new WindowDefinition();
13521+
String baseWindowName;
1350113522
} {
1350213523
"("
13524+
// DuckDB: (w ORDER BY b) extends the named window w. A choice, not an optional, so that
13525+
// lookahead scans through a window specification keep the check
13526+
(
13527+
LOOKAHEAD({ isBaseWindowNameAhead() }) baseWindowName=RelObjectName()
13528+
{ winDef.setBaseWindowName(baseWindowName); }
13529+
|
13530+
{}
13531+
)
1350313532
[<K_PARTITION> <K_BY>
1350413533
(LOOKAHEAD(3) expressionList=ComplexExpressionList()
1350513534
| "(" {partitionByBrackets = true;} expressionList=ComplexExpressionList() ")" )
@@ -13523,7 +13552,7 @@ AnalyticExpression AnalyticExpression(Function function) :
1352313552
{
1352413553
(
1352513554
(
13526-
filter=AggregateFilter() { retval.setType(AnalyticType.FILTER_ONLY); }
13555+
filter=AggregateFilter(retval) { retval.setType(AnalyticType.FILTER_ONLY); }
1352713556
[ LOOKAHEAD(2) windowFun(retval) ]
1352813557
{
1352913558
if (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect))
@@ -13535,19 +13564,29 @@ AnalyticExpression AnalyticExpression(Function function) :
1353513564
)
1353613565
| windowFun(retval)
1353713566
[ LOOKAHEAD({ retval.getType() == AnalyticType.WITHIN_GROUP
13538-
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter() ]
13567+
&& getToken(1).kind == K_FILTER }) filter=AggregateFilter(retval) ]
1353913568
)
1354013569
{
1354113570
retval.setFilterExpression(filter);
1354213571
if (true) { return retval; }
1354313572
}
1354413573
}
1354513574

13546-
/** Shared FILTER predicate for ordinary, window, and ordered-set aggregates. */
13547-
Expression AggregateFilter():
13575+
/**
13576+
* Shared FILTER predicate for ordinary, window, and ordered-set aggregates. DuckDB also accepts
13577+
* it without WHERE, FILTER (x > 1), but PostgreSQL does not.
13578+
*/
13579+
Expression AggregateFilter(AnalyticExpression analyticExpression):
1354813580
{ Expression filter; }
1354913581
{
13550-
<K_FILTER> "(" <K_WHERE> filter=Expression() ")"
13582+
<K_FILTER> "("
13583+
(
13584+
<K_WHERE>
13585+
|
13586+
LOOKAHEAD({ !Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
13587+
{ analyticExpression.setFilterWhereKeyword(false); }
13588+
)
13589+
filter=Expression() ")"
1355113590
{ return filter; }
1355213591
}
1355313592

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,4 +1072,38 @@ 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+
// window specifications inside syntactic lookahead
1088+
"SELECT JSON_VALUE(j, '$.a' DEFAULT sum(a) OVER (PARTITION BY b) ON EMPTY) FROM t",
1089+
"SELECT JSON_VALUE(j, '$.a' DEFAULT sum(a) OVER (w ORDER BY b) ON EMPTY) FROM t"}) {
1090+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
1091+
}
1092+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
1093+
"SELECT sum(a) FILTER (b > 1) OVER (w ORDER BY c) FROM t WINDOW w AS (PARTITION BY d)");
1094+
net.sf.jsqlparser.expression.AnalyticExpression sum =
1095+
(net.sf.jsqlparser.expression.AnalyticExpression) select.getSelectItem(0)
1096+
.getExpression();
1097+
Assertions.assertFalse(sum.isFilterWhereKeyword());
1098+
Assertions.assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(
1099+
"SELECT percentile_cont(0.5) WITHIN GROUP (w ORDER BY a) FROM t"));
1100+
Assertions.assertThrows(JSQLParserException.class,
1101+
() -> CCJSqlParserUtil.parse("SELECT a FROM t ORDER SIBLINGS BY a QUALIFY x = 1"));
1102+
// PostgreSQL requires WHERE
1103+
Assertions.assertThrows(JSQLParserException.class,
1104+
() -> CCJSqlParserUtil.parse("SELECT sum(a) FILTER (b > 1) FROM t",
1105+
parser -> parser.withDialect(
1106+
net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.POSTGRESQL)));
1107+
Assertions.assertEquals("w", sum.getWindowDefinition().getBaseWindowName());
1108+
}
10751109
}

0 commit comments

Comments
 (0)