diff --git a/src/main/java/net/sf/jsqlparser/expression/WindowElement.java b/src/main/java/net/sf/jsqlparser/expression/WindowElement.java index cacb246a4..01f77557c 100644 --- a/src/main/java/net/sf/jsqlparser/expression/WindowElement.java +++ b/src/main/java/net/sf/jsqlparser/expression/WindowElement.java @@ -17,6 +17,7 @@ public class WindowElement implements Serializable { private Type type; private WindowOffset offset; private WindowRange range; + private Exclusion exclusion; public Type getType() { return type; @@ -42,6 +43,14 @@ public void setRange(WindowRange range) { this.range = range; } + public Exclusion getExclusion() { + return exclusion; + } + + public void setExclusion(Exclusion exclusion) { + this.exclusion = exclusion; + } + @Override public String toString() { StringBuilder buffer = new StringBuilder(type.toString()); @@ -52,6 +61,10 @@ public String toString() { buffer.append(range.toString()); } + if (exclusion != null) { + buffer.append(" EXCLUDE ").append(exclusion); + } + return buffer.toString(); } @@ -70,12 +83,32 @@ public WindowElement withRange(WindowRange range) { return this; } + public WindowElement withExclusion(Exclusion exclusion) { + this.setExclusion(exclusion); + return this; + } + public enum Type { - ROWS, RANGE; + ROWS, RANGE, GROUPS; public static Type from(String type) { return Enum.valueOf(Type.class, type.toUpperCase(Locale.ROOT)); } } + public enum Exclusion { + CURRENT_ROW("CURRENT ROW"), GROUP("GROUP"), TIES("TIES"), NO_OTHERS("NO OTHERS"); + + private final String keyword; + + Exclusion(String keyword) { + this.keyword = keyword; + } + + @Override + public String toString() { + return keyword; + } + } + } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index fea018779..5c7c38d23 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1443,6 +1443,7 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | +| | | | @@ -9203,9 +9204,12 @@ WindowElement WindowElement(): WindowElement windowElement = new WindowElement(); WindowRange range = new WindowRange(); WindowOffset offset = null; + WindowElement.Exclusion exclusion = null; } { - ( { windowElement.setType(WindowElement.Type.ROWS); } | { windowElement.setType(WindowElement.Type.RANGE); } ) + ( { windowElement.setType(WindowElement.Type.ROWS); } + | { windowElement.setType(WindowElement.Type.RANGE); } + | { windowElement.setType(WindowElement.Type.GROUPS); }) ( ( { windowElement.setRange(range); } offset = WindowOffset() { range.setStart(offset); } @@ -9214,12 +9218,44 @@ WindowElement WindowElement(): | offset = WindowOffset() { windowElement.setOffset(offset); } ) + [ exclusion = FrameExclusion() { windowElement.setExclusion(exclusion); } ] { return windowElement; } } +WindowElement.Exclusion FrameExclusion(): +{ + WindowElement.Exclusion exclusion = null; +} +{ + + ( + + { exclusion = WindowElement.Exclusion.CURRENT_ROW; } + | + + { exclusion = WindowElement.Exclusion.GROUP; } + | + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("TIES") + }) + + { exclusion = WindowElement.Exclusion.TIES; } + | + + LOOKAHEAD({ + getToken(1).kind == S_IDENTIFIER + && getToken(1).image.equalsIgnoreCase("OTHERS") + }) + + { exclusion = WindowElement.Exclusion.NO_OTHERS; } + ) + { return exclusion; } +} + WindowOffset WindowOffset(): { WindowOffset offset = new WindowOffset(); diff --git a/src/test/java/net/sf/jsqlparser/statement/select/WindowFunctionTest.java b/src/test/java/net/sf/jsqlparser/statement/select/WindowFunctionTest.java index 6a0beb7ac..f4e0b5c10 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/WindowFunctionTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/WindowFunctionTest.java @@ -9,7 +9,15 @@ */ package net.sf.jsqlparser.statement.select; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AnalyticExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.WindowElement; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; @@ -38,4 +46,71 @@ public void RedshiftRespectIgnoreNulls() throws JSQLParserException { TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); } + + @Test + public void testWindowFrameGroupsIssue2431() throws JSQLParserException { + String sqlString = + "SELECT SUM(value) OVER (ORDER BY ts " + + "GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM events"; + + WindowElement windowElement = parseWindowElement(sqlString, 0); + assertEquals(WindowElement.Type.GROUPS, windowElement.getType()); + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); + } + + @Test + public void testWindowFrameGroupsExcludeTiesIssue2431() throws JSQLParserException { + String sqlString = + "SELECT id, ts, value, SUM(value) OVER (ORDER BY ts " + + "GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW EXCLUDE TIES) AS sum_excl_ties " + + "FROM events ORDER BY ts, id"; + + WindowElement windowElement = parseWindowElement(sqlString, 3); + assertEquals(WindowElement.Exclusion.TIES, windowElement.getExclusion()); + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); + } + + @Test + public void testWindowFrameExclusionsIssue2431() throws JSQLParserException { + String[] sqlStrings = { + "SELECT SUM(value) OVER (ORDER BY ts ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) FROM events", + "SELECT SUM(value) OVER (ORDER BY ts RANGE CURRENT ROW EXCLUDE GROUP) FROM events", + "SELECT SUM(value) OVER (ORDER BY ts GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW EXCLUDE TIES) FROM events", + "SELECT SUM(value) OVER (ORDER BY ts GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) FROM events" + }; + + for (String sqlString : sqlStrings) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlString, true); + } + } + + @Test + public void testFrameExclusionIdentifierCompatibilityIssue2431() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT ties FROM ties", true); + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT others FROM others", true); + } + + @Test + public void testWindowFrameGroupsVariantsIssue2431() throws JSQLParserException { + String singleSidedSqlString = + "SELECT SUM(value) OVER (ORDER BY ts GROUPS UNBOUNDED PRECEDING) FROM events"; + String namedWindowSqlString = + "SELECT SUM(value) OVER w FROM events " + + "WINDOW w AS (ORDER BY ts GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW)"; + + TestUtils.assertSqlCanBeParsedAndDeparsed(singleSidedSqlString, true); + TestUtils.assertSqlCanBeParsedAndDeparsed(namedWindowSqlString, true); + } + + private WindowElement parseWindowElement(String sqlString, int selectItemIndex) + throws JSQLParserException { + PlainSelect plainSelect = (PlainSelect) CCJSqlParserUtil.parse(sqlString); + Expression expression = plainSelect.getSelectItem(selectItemIndex).getExpression(); + assertInstanceOf(AnalyticExpression.class, expression); + AnalyticExpression analyticExpression = (AnalyticExpression) expression; + WindowElement windowElement = analyticExpression.getWindowElement(); + + assertNotNull(windowElement); + return windowElement; + } }