Skip to content

Commit d92f407

Browse files
authored
fix: support PostgreSQL GROUPS window frames (#2460)
* fix: support PostgreSQL GROUPS window frames (#2431) * fix: support window frame exclusions
1 parent b444bc2 commit d92f407

3 files changed

Lines changed: 146 additions & 2 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/WindowElement.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class WindowElement implements Serializable {
1717
private Type type;
1818
private WindowOffset offset;
1919
private WindowRange range;
20+
private Exclusion exclusion;
2021

2122
public Type getType() {
2223
return type;
@@ -42,6 +43,14 @@ public void setRange(WindowRange range) {
4243
this.range = range;
4344
}
4445

46+
public Exclusion getExclusion() {
47+
return exclusion;
48+
}
49+
50+
public void setExclusion(Exclusion exclusion) {
51+
this.exclusion = exclusion;
52+
}
53+
4554
@Override
4655
public String toString() {
4756
StringBuilder buffer = new StringBuilder(type.toString());
@@ -52,6 +61,10 @@ public String toString() {
5261
buffer.append(range.toString());
5362
}
5463

64+
if (exclusion != null) {
65+
buffer.append(" EXCLUDE ").append(exclusion);
66+
}
67+
5568
return buffer.toString();
5669
}
5770

@@ -70,12 +83,32 @@ public WindowElement withRange(WindowRange range) {
7083
return this;
7184
}
7285

86+
public WindowElement withExclusion(Exclusion exclusion) {
87+
this.setExclusion(exclusion);
88+
return this;
89+
}
90+
7391
public enum Type {
74-
ROWS, RANGE;
92+
ROWS, RANGE, GROUPS;
7593

7694
public static Type from(String type) {
7795
return Enum.valueOf(Type.class, type.toUpperCase(Locale.ROOT));
7896
}
7997
}
8098

99+
public enum Exclusion {
100+
CURRENT_ROW("CURRENT ROW"), GROUP("GROUP"), TIES("TIES"), NO_OTHERS("NO OTHERS");
101+
102+
private final String keyword;
103+
104+
Exclusion(String keyword) {
105+
this.keyword = keyword;
106+
}
107+
108+
@Override
109+
public String toString() {
110+
return keyword;
111+
}
112+
}
113+
81114
}

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,7 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */
14431443
| <K_GLOBAL:"GLOBAL">
14441444
| <K_GROUP:"GROUP">
14451445
| <K_GROUPING:"GROUPING">
1446+
| <K_GROUPS:"GROUPS">
14461447
| <K_HAVING:"HAVING">
14471448
| <K_IF:"IF">
14481449
| <K_IIF:"IIF">
@@ -9203,9 +9204,12 @@ WindowElement WindowElement():
92039204
WindowElement windowElement = new WindowElement();
92049205
WindowRange range = new WindowRange();
92059206
WindowOffset offset = null;
9207+
WindowElement.Exclusion exclusion = null;
92069208
}
92079209
{
9208-
(<K_ROWS> { windowElement.setType(WindowElement.Type.ROWS); } | <K_RANGE> { windowElement.setType(WindowElement.Type.RANGE); } )
9210+
(<K_ROWS> { windowElement.setType(WindowElement.Type.ROWS); }
9211+
| <K_RANGE> { windowElement.setType(WindowElement.Type.RANGE); }
9212+
| <K_GROUPS> { windowElement.setType(WindowElement.Type.GROUPS); })
92099213
( (
92109214
<K_BETWEEN> { windowElement.setRange(range); }
92119215
offset = WindowOffset() { range.setStart(offset); }
@@ -9214,12 +9218,44 @@ WindowElement WindowElement():
92149218
|
92159219
offset = WindowOffset() { windowElement.setOffset(offset); }
92169220
)
9221+
[ exclusion = FrameExclusion() { windowElement.setExclusion(exclusion); } ]
92179222

92189223
{
92199224
return windowElement;
92209225
}
92219226
}
92229227

9228+
WindowElement.Exclusion FrameExclusion():
9229+
{
9230+
WindowElement.Exclusion exclusion = null;
9231+
}
9232+
{
9233+
<K_EXCLUDE>
9234+
(
9235+
<K_CURRENT> <K_ROW>
9236+
{ exclusion = WindowElement.Exclusion.CURRENT_ROW; }
9237+
|
9238+
<K_GROUP>
9239+
{ exclusion = WindowElement.Exclusion.GROUP; }
9240+
|
9241+
LOOKAHEAD({
9242+
getToken(1).kind == S_IDENTIFIER
9243+
&& getToken(1).image.equalsIgnoreCase("TIES")
9244+
})
9245+
<S_IDENTIFIER>
9246+
{ exclusion = WindowElement.Exclusion.TIES; }
9247+
|
9248+
<K_NO>
9249+
LOOKAHEAD({
9250+
getToken(1).kind == S_IDENTIFIER
9251+
&& getToken(1).image.equalsIgnoreCase("OTHERS")
9252+
})
9253+
<S_IDENTIFIER>
9254+
{ exclusion = WindowElement.Exclusion.NO_OTHERS; }
9255+
)
9256+
{ return exclusion; }
9257+
}
9258+
92239259
WindowOffset WindowOffset():
92249260
{
92259261
WindowOffset offset = new WindowOffset();

src/test/java/net/sf/jsqlparser/statement/select/WindowFunctionTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@
99
*/
1010
package net.sf.jsqlparser.statement.select;
1111

12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
1216
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.AnalyticExpression;
18+
import net.sf.jsqlparser.expression.Expression;
19+
import net.sf.jsqlparser.expression.WindowElement;
20+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1321
import net.sf.jsqlparser.test.TestUtils;
1422
import org.junit.jupiter.api.Test;
1523

@@ -38,4 +46,71 @@ public void RedshiftRespectIgnoreNulls() throws JSQLParserException {
3846

3947
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true);
4048
}
49+
50+
@Test
51+
public void testWindowFrameGroupsIssue2431() throws JSQLParserException {
52+
String sqlString =
53+
"SELECT SUM(value) OVER (ORDER BY ts "
54+
+ "GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM events";
55+
56+
WindowElement windowElement = parseWindowElement(sqlString, 0);
57+
assertEquals(WindowElement.Type.GROUPS, windowElement.getType());
58+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true);
59+
}
60+
61+
@Test
62+
public void testWindowFrameGroupsExcludeTiesIssue2431() throws JSQLParserException {
63+
String sqlString =
64+
"SELECT id, ts, value, SUM(value) OVER (ORDER BY ts "
65+
+ "GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW EXCLUDE TIES) AS sum_excl_ties "
66+
+ "FROM events ORDER BY ts, id";
67+
68+
WindowElement windowElement = parseWindowElement(sqlString, 3);
69+
assertEquals(WindowElement.Exclusion.TIES, windowElement.getExclusion());
70+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true);
71+
}
72+
73+
@Test
74+
public void testWindowFrameExclusionsIssue2431() throws JSQLParserException {
75+
String[] sqlStrings = {
76+
"SELECT SUM(value) OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM events",
77+
"SELECT SUM(value) OVER (ORDER BY ts RANGE CURRENT ROW EXCLUDE GROUP) FROM events",
78+
"SELECT SUM(value) OVER (ORDER BY ts GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW EXCLUDE TIES) FROM events",
79+
"SELECT SUM(value) OVER (ORDER BY ts GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) FROM events"
80+
};
81+
82+
for (String sqlString : sqlStrings) {
83+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true);
84+
}
85+
}
86+
87+
@Test
88+
public void testFrameExclusionIdentifierCompatibilityIssue2431() throws JSQLParserException {
89+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT ties FROM ties", true);
90+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT others FROM others", true);
91+
}
92+
93+
@Test
94+
public void testWindowFrameGroupsVariantsIssue2431() throws JSQLParserException {
95+
String singleSidedSqlString =
96+
"SELECT SUM(value) OVER (ORDER BY ts GROUPS UNBOUNDED PRECEDING) FROM events";
97+
String namedWindowSqlString =
98+
"SELECT SUM(value) OVER w FROM events "
99+
+ "WINDOW w AS (ORDER BY ts GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW)";
100+
101+
TestUtils.assertSqlCanBeParsedAndDeparsed(singleSidedSqlString, true);
102+
TestUtils.assertSqlCanBeParsedAndDeparsed(namedWindowSqlString, true);
103+
}
104+
105+
private WindowElement parseWindowElement(String sqlString, int selectItemIndex)
106+
throws JSQLParserException {
107+
PlainSelect plainSelect = (PlainSelect) CCJSqlParserUtil.parse(sqlString);
108+
Expression expression = plainSelect.getSelectItem(selectItemIndex).getExpression();
109+
assertInstanceOf(AnalyticExpression.class, expression);
110+
AnalyticExpression analyticExpression = (AnalyticExpression) expression;
111+
WindowElement windowElement = analyticExpression.getWindowElement();
112+
113+
assertNotNull(windowElement);
114+
return windowElement;
115+
}
41116
}

0 commit comments

Comments
 (0)