diff --git a/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java b/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java new file mode 100644 index 000000000..a571c4cf5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java @@ -0,0 +1,73 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; + +/** + * Models one column entry of the ClickHouse {@code INTERPOLATE} clause, which computes the values + * of non-sorted columns for the rows generated by {@code ORDER BY ... WITH FILL} (see + * ClickHouse: + * ORDER BY). + *

+ * {@code col AS expr} evaluates {@code expr} for the generated rows, while a bare {@code col} (a + * {@code null} expression) repeats the previous value of the column. + */ +public class InterpolateElement implements Serializable { + + private Column column; + private Expression expression; + + public InterpolateElement() { + super(); + } + + public InterpolateElement(Column column, Expression expression) { + this.column = column; + this.expression = expression; + } + + public Column getColumn() { + return column; + } + + public void setColumn(Column column) { + this.column = column; + } + + public InterpolateElement withColumn(Column column) { + setColumn(column); + return this; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public InterpolateElement withExpression(Expression expression) { + setExpression(expression); + return this; + } + + @Override + public String toString() { + if (expression != null) { + return column + " AS " + expression; + } + return column.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java b/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java index 38b78384d..4662150fa 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java @@ -22,6 +22,7 @@ public class OrderByElement implements Serializable { private boolean asc = true; private boolean ascDescPresent = false; private NullOrdering nullOrdering; + private WithFill withFill; public boolean isAsc() { return asc; @@ -39,6 +40,20 @@ public void setNullOrdering(NullOrdering nullOrdering) { this.nullOrdering = nullOrdering; } + public WithFill getWithFill() { + return withFill; + } + + public OrderByElement setWithFill(WithFill withFill) { + this.withFill = withFill; + return this; + } + + public OrderByElement withWithFill(WithFill withFill) { + setWithFill(withFill); + return this; + } + public boolean isAscDescPresent() { return ascDescPresent; } @@ -74,6 +89,9 @@ public String toString() { b.append(' '); b.append(nullOrdering == NullOrdering.NULLS_FIRST ? "NULLS FIRST" : "NULLS LAST"); } + if (withFill != null) { + b.append(' ').append(withFill); + } if (isMysqlWithRollup()) { b.append(" WITH ROLLUP"); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 737608020..0dc57e32a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -36,6 +36,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp ForClause forClause = null; List orderByElements; + List interpolate; ForMode forMode = null; private boolean skipLocked; private Wait wait; @@ -54,6 +55,16 @@ public static String orderByToString(boolean oracleSiblings, return getFormattedList(orderByElements, oracleSiblings ? "ORDER SIBLINGS BY" : "ORDER BY"); } + public static String interpolateToString(List interpolate) { + if (interpolate == null) { + return ""; + } + if (interpolate.isEmpty()) { + return " INTERPOLATE"; + } + return getFormattedList(interpolate, "INTERPOLATE", true, true); + } + public static String getFormattedList(List list, String expression) { return getFormattedList(list, expression, true, false); } @@ -192,6 +203,19 @@ public void setOrderByElements(List orderByElements) { this.orderByElements = orderByElements; } + public List getInterpolate() { + return interpolate; + } + + public void setInterpolate(List interpolate) { + this.interpolate = interpolate; + } + + public Select withInterpolate(List interpolate) { + setInterpolate(interpolate); + return this; + } + public Select withOrderByElements(List orderByElements) { this.setOrderByElements(orderByElements); return this; @@ -463,6 +487,7 @@ public StringBuilder appendTo(StringBuilder builder) { if (!forUpdateBeforeOrderBy) { builder.append(orderByToString(oracleSiblings, orderByElements)); + builder.append(interpolateToString(interpolate)); } if (forClause != null) { @@ -512,6 +537,7 @@ public StringBuilder appendTo(StringBuilder builder) { if (forUpdateBeforeOrderBy) { builder.append(orderByToString(oracleSiblings, orderByElements)); + builder.append(interpolateToString(interpolate)); } return builder; diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java b/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java new file mode 100644 index 000000000..9da567251 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithFill.java @@ -0,0 +1,102 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; + +import net.sf.jsqlparser.expression.Expression; + +/** + * Models the ClickHouse {@code WITH FILL} modifier of an {@link OrderByElement}, which fills gaps + * in the sorted sequence of the order by expression (see + * ClickHouse: + * ORDER BY). + *

+ * The modifier takes optional {@code FROM}, {@code TO}, {@code STEP} and {@code STALENESS} bounds, + * where the step may be an {@code INTERVAL} literal. Its presence on an {@link OrderByElement} + * marks the element as filled. + */ +public class WithFill implements Serializable { + + private Expression from; + private Expression to; + private Expression step; + private Expression staleness; + + public Expression getFrom() { + return from; + } + + public void setFrom(Expression from) { + this.from = from; + } + + public WithFill withFrom(Expression from) { + setFrom(from); + return this; + } + + public Expression getTo() { + return to; + } + + public void setTo(Expression to) { + this.to = to; + } + + public WithFill withTo(Expression to) { + setTo(to); + return this; + } + + public Expression getStep() { + return step; + } + + public void setStep(Expression step) { + this.step = step; + } + + public WithFill withStep(Expression step) { + setStep(step); + return this; + } + + public Expression getStaleness() { + return staleness; + } + + public void setStaleness(Expression staleness) { + this.staleness = staleness; + } + + public WithFill withStaleness(Expression staleness) { + setStaleness(staleness); + return this; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder("WITH FILL"); + if (from != null) { + b.append(" FROM ").append(from); + } + if (to != null) { + b.append(" TO ").append(to); + } + if (step != null) { + b.append(" STEP ").append(step); + } + if (staleness != null) { + b.append(" STALENESS ").append(staleness); + } + return b.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java index 605c54dc2..66e20bebb 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.WithFill; public class OrderByDeParser extends AbstractDeParser> { @@ -64,11 +65,35 @@ public void deParseElement(OrderByElement orderBy) { ? "NULLS FIRST" : "NULLS LAST"); } + if (orderBy.getWithFill() != null) { + builder.append(' '); + deParseWithFill(orderBy.getWithFill()); + } if (orderBy.isMysqlWithRollup()) { builder.append(" WITH ROLLUP"); } } + private void deParseWithFill(WithFill withFill) { + builder.append("WITH FILL"); + if (withFill.getFrom() != null) { + builder.append(" FROM "); + withFill.getFrom().accept(expressionVisitor, null); + } + if (withFill.getTo() != null) { + builder.append(" TO "); + withFill.getTo().accept(expressionVisitor, null); + } + if (withFill.getStep() != null) { + builder.append(" STEP "); + withFill.getStep().accept(expressionVisitor, null); + } + if (withFill.getStaleness() != null) { + builder.append(" STALENESS "); + withFill.getStaleness().accept(expressionVisitor, null); + } + } + void setExpressionVisitor(ExpressionVisitor expressionVisitor) { this.expressionVisitor = expressionVisitor; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 50edf3b70..46421c1c8 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -51,6 +51,7 @@ import net.sf.jsqlparser.statement.select.First; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; import net.sf.jsqlparser.statement.select.LateralView; @@ -136,6 +137,7 @@ public StringBuilder visit(ParenthesedSelect select, S context) { if (select.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder).deParse(select.isOracleSiblings(), select.getOrderByElements()); + deParseInterpolate(select.getInterpolate()); } Alias alias = select.getAlias(); @@ -471,9 +473,33 @@ protected void deparseOrderByElementsClause(PlainSelect plainSelect, if (orderByElements != null) { new OrderByDeParser(expressionVisitor, builder).deParse(plainSelect.isOracleSiblings(), orderByElements); + deParseInterpolate(plainSelect.getInterpolate()); } } + private void deParseInterpolate(List interpolate) { + if (interpolate == null) { + return; + } + if (interpolate.isEmpty()) { + builder.append(" INTERPOLATE"); + return; + } + builder.append(" INTERPOLATE ("); + for (Iterator iter = interpolate.iterator(); iter.hasNext();) { + InterpolateElement element = iter.next(); + element.getColumn().accept(expressionVisitor, null); + if (element.getExpression() != null) { + builder.append(" AS "); + element.getExpression().accept(expressionVisitor, null); + } + if (iter.hasNext()) { + builder.append(", "); + } + } + builder.append(")"); + } + @Override public StringBuilder visit(SelectItem selectItem, S context) { selectItem.getExpression().accept(expressionVisitor, context); @@ -729,6 +755,7 @@ public StringBuilder visit(SetOperationList list, S context) { } if (list.getOrderByElements() != null) { new OrderByDeParser(expressionVisitor, builder).deParse(list.getOrderByElements()); + deParseInterpolate(list.getInterpolate()); } if (list.getLimit() != null) { diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java index 858702dda..8e04d7df9 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java @@ -32,6 +32,12 @@ public Void visit(OrderByElement orderBy, S context) { validateOptionalFeature(c, orderBy.getNullOrdering(), Feature.orderByNullOrdering); } getValidator(ExpressionValidator.class).validate(orderBy.getExpression()); + if (orderBy.getWithFill() != null) { + validateOptionalExpression(orderBy.getWithFill().getFrom()); + validateOptionalExpression(orderBy.getWithFill().getTo()); + validateOptionalExpression(orderBy.getWithFill().getStep()); + validateOptionalExpression(orderBy.getWithFill().getStaleness()); + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index 5cab04030..92334db52 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -21,6 +21,7 @@ import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.ForMode; import net.sf.jsqlparser.statement.select.FromItemVisitor; +import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.IntersectOp; import net.sf.jsqlparser.statement.select.Join; import net.sf.jsqlparser.statement.select.LateralSubSelect; @@ -136,6 +137,7 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalExpression(plainSelect.getHaving()); validateOptionalOrderByElements(plainSelect.getOrderByElements()); + validateOptionalInterpolate(plainSelect.getInterpolate()); if (plainSelect.getLimit() != null) { getValidator(LimitValidator.class).validate(plainSelect.getLimit()); @@ -163,6 +165,15 @@ private void validateMySqlSelectIntoClause(MySqlSelectIntoClause mySqlSelectInto validateOptionalExpression(mySqlSelectIntoClause.getLinesTerminatedBy()); } + private void validateOptionalInterpolate(List interpolate) { + if (interpolate != null) { + for (InterpolateElement element : interpolate) { + validateOptionalExpression(element.getColumn()); + validateOptionalExpression(element.getExpression()); + } + } + } + @Override public Void visit(SelectItem selectExpressionItem, S context) { selectExpressionItem.getExpression().accept(getValidator(ExpressionValidator.class), @@ -308,6 +319,7 @@ public Void visit(SetOperationList setOperation, S context) { } validateOptionalOrderByElements(setOperation.getOrderByElements()); + validateOptionalInterpolate(setOperation.getInterpolate()); if (setOperation.getLimit() != null) { getValidator(LimitValidator.class).validate(setOperation.getLimit()); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 99e62720f..5f8e91d11 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1279,6 +1279,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1303,6 +1304,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1453,7 +1455,9 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= + | tk= | tk= | tk= | tk= @@ -4686,6 +4690,7 @@ Select Select() #Select: Select select = null; List> with = null; List orderByElements = null; + List interpolateElements = null; Limit limit = null; Offset offset = null; Fetch fetch = null; @@ -4693,7 +4698,6 @@ Select Select() #Select: Alias alias = null; } { - [ with=WithList() ] ( LOOKAHEAD(3) select = FromQuery() @@ -4710,6 +4714,7 @@ Select Select() #Select: [ LOOKAHEAD(2) select = SetOperationList(select) ] [ LOOKAHEAD( ) orderByElements = OrderByElements() { select.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { select.setInterpolate(interpolateElements); } ] [ LOOKAHEAD() limit=LimitWithOffset() {select.setLimit(limit);} ] [ LOOKAHEAD() offset = Offset() { select.setOffset(offset);} ] @@ -5334,6 +5339,7 @@ PlainSelect PlainSelect() #PlainSelect: Expression where = null; ForClause forClause = null; List orderByElements; + List interpolateElements = null; GroupByElement groupBy = null; Expression having = null; Expression qualify; @@ -5462,6 +5468,7 @@ PlainSelect PlainSelect() #PlainSelect: { plainSelect.setWindowDefinitions(winDefs); } ] [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] [ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ] [ LOOKAHEAD(2) { plainSelect.setEmitChanges(true); } ] // Parse the LIMIT row count once (this accepts a parenthesized subquery too), then @@ -5502,6 +5509,7 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); } ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { plainSelect.setInterpolate(interpolateElements); } ] ] [ LOOKAHEAD( ( | )) mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING) @@ -5528,6 +5536,7 @@ PlainSelect PlainSelect() #PlainSelect: Select SetOperationList(Select select) #SetOperationList: { SetOperationList list = new SetOperationList(); List orderByElements = null; + List interpolateElements = null; Limit limit = null; Offset offset = null; Fetch fetch = null; @@ -5575,6 +5584,7 @@ Select SetOperationList(Select select) #SetOperationList: { )+ [ LOOKAHEAD(2) orderByElements=OrderByElements() {list.setOrderByElements(orderByElements);} ] + [ LOOKAHEAD(2) interpolateElements = InterpolateClause() { list.setInterpolate(interpolateElements); } ] [ LOOKAHEAD() limit = LimitWithOffset() { list.setLimit(limit); } ] [ LOOKAHEAD() offset = Offset() { list.setOffset(offset); } ] [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { list.setLimit(limit); } ] @@ -5592,6 +5602,10 @@ Select SetOperationList(Select select) #SetOperationList: { ps.setLimit(null); ps.setOffset(null); } + if (ps.getInterpolate() != null) { + list.setInterpolate(ps.getInterpolate()); + ps.setInterpolate(null); + } if (ps.getFetch() != null) { list.setFetch(ps.getFetch()); ps.setFetch(null); @@ -6778,6 +6792,7 @@ OrderByElement OrderByElement(): OrderByElement orderByElement = new OrderByElement(); Expression columnReference = null; Token collateToken = null; + WithFill withFill = null; } { columnReference = Expression() @@ -6791,6 +6806,7 @@ OrderByElement OrderByElement(): ) ] ] + [ LOOKAHEAD(2) withFill = WithFill() { orderByElement.setWithFill(withFill); } ] [ LOOKAHEAD(2) { orderByElement.setMysqlWithRollup(true); } ] { orderByElement.setExpression(columnReference); @@ -6798,6 +6814,47 @@ OrderByElement OrderByElement(): } } +WithFill WithFill(): +{ + WithFill withFill = new WithFill(); + Expression fillExpression = null; +} +{ + + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setFrom(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setTo(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setStep(fillExpression); } ] + [ LOOKAHEAD(2) fillExpression = Expression() { withFill.setStaleness(fillExpression); } ] + { return withFill; } +} + +List InterpolateClause(): +{ + List interpolateElements = new ArrayList(); + InterpolateElement interpolateElement = null; +} +{ + + [ "(" + [ interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } + ( "," interpolateElement = InterpolateElement() { interpolateElements.add(interpolateElement); } )* + ] + ")" ] + { return interpolateElements; } +} + +InterpolateElement InterpolateElement(): +{ + InterpolateElement interpolateElement = new InterpolateElement(); + Column column = null; + Expression expression = null; +} +{ + column = Column() { interpolateElement.setColumn(column); } + [ expression = Expression() { interpolateElement.setExpression(expression); } ] + { return interpolateElement; } +} + JdbcParameter JdbcParameter() : { Token tk; JdbcParameter retval; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java index 90f69ffd4..cfdca4e77 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/ClickHouseTest.java @@ -147,6 +147,105 @@ public void testMultipleSettingsClauseIssue2362() throws JSQLParserException { Assertions.assertEquals(2, select.getSettings().size()); } + @Test + public void testOrderByWithFillIssue2467() throws JSQLParserException { + String sql = + "SELECT d, v FROM t ORDER BY d WITH FILL FROM toDate('2024-01-01') TO toDate('2024-02-01') STEP INTERVAL 1 DAY INTERPOLATE (v AS v + 1)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + + WithFill withFill = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertNotNull(withFill); + Assertions.assertEquals("toDate('2024-01-01')", withFill.getFrom().toString()); + Assertions.assertEquals("toDate('2024-02-01')", withFill.getTo().toString()); + Assertions.assertEquals("INTERVAL 1 DAY", withFill.getStep().toString()); + Assertions.assertNull(withFill.getStaleness()); + + Assertions.assertEquals(1, select.getInterpolate().size()); + Assertions.assertEquals("v", select.getInterpolate().get(0).getColumn().getColumnName()); + Assertions.assertEquals("v + 1", select.getInterpolate().get(0).getExpression().toString()); + } + + @Test + public void testOrderByWithFillOnly() throws JSQLParserException { + String sql = "SELECT key, value FROM t ORDER BY key WITH FILL"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getOrderByElements().get(0).getWithFill()); + Assertions.assertNull(select.getOrderByElements().get(0).getWithFill().getFrom()); + } + + @Test + public void testOrderByWithFillFromToStep() throws JSQLParserException { + String sql = "SELECT n FROM t ORDER BY n WITH FILL FROM 0 TO 5.51 STEP 0.5"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + WithFill withFill = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertEquals("0", withFill.getFrom().toString()); + Assertions.assertEquals("5.51", withFill.getTo().toString()); + Assertions.assertEquals("0.5", withFill.getStep().toString()); + } + + @Test + public void testOrderByWithFillStepIntervalAndStaleness() throws JSQLParserException { + String sql = + "SELECT d1, d2 FROM t ORDER BY d1 WITH FILL STEP INTERVAL 1 DAY STALENESS 3, d2 WITH FILL"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(2, select.getOrderByElements().size()); + WithFill first = select.getOrderByElements().get(0).getWithFill(); + Assertions.assertEquals("INTERVAL 1 DAY", first.getStep().toString()); + Assertions.assertEquals("3", first.getStaleness().toString()); + Assertions.assertNotNull(select.getOrderByElements().get(1).getWithFill()); + } + + @Test + public void testOrderByWithFillDescNullsLast() throws JSQLParserException { + String sql = "SELECT d FROM t ORDER BY d DESC NULLS LAST WITH FILL TO 10"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + OrderByElement element = select.getOrderByElements().get(0); + Assertions.assertFalse(element.isAsc()); + Assertions.assertEquals(OrderByElement.NullOrdering.NULLS_LAST, element.getNullOrdering()); + Assertions.assertEquals("10", element.getWithFill().getTo().toString()); + } + + @Test + public void testOrderByWithFillInterpolateVariants() throws JSQLParserException { + // multiple items, with and without AS + String sql = "SELECT a, b, c FROM t ORDER BY a WITH FILL INTERPOLATE (b, c AS c + 1)"; + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertEquals(2, select.getInterpolate().size()); + Assertions.assertNull(select.getInterpolate().get(0).getExpression()); + Assertions.assertEquals("c + 1", select.getInterpolate().get(1).getExpression().toString()); + + // bare INTERPOLATE fills all allowed columns + sql = "SELECT a, b FROM t ORDER BY a WITH FILL INTERPOLATE"; + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, true); + Assertions.assertNotNull(select.getInterpolate()); + Assertions.assertTrue(select.getInterpolate().isEmpty()); + } + + @Test + public void testOrderByWithFillUnionInterpolate() throws JSQLParserException { + String sql = + "SELECT a FROM t1 UNION SELECT a FROM t2 ORDER BY a WITH FILL INTERPOLATE (a AS a + 1) LIMIT 10"; + Select select = (Select) assertSqlCanBeParsedAndDeparsed(sql, true); + SetOperationList setOperationList = (SetOperationList) select.getSelectBody(); + Assertions.assertNotNull(setOperationList.getOrderByElements().get(0).getWithFill()); + Assertions.assertEquals(1, setOperationList.getInterpolate().size()); + } + + @Test + public void testOrderByWithFillSubquery() throws JSQLParserException { + String sql = "SELECT * FROM (SELECT d, v FROM t ORDER BY d WITH FILL FROM 0 TO 9) AS sub"; + assertSqlCanBeParsedAndDeparsed(sql, true); + } + + @Test + public void testFillStepInterpolateStalenessAsColumnNames() throws JSQLParserException { + // the new keywords stay non-reserved and must remain usable as plain column names + assertSqlCanBeParsedAndDeparsed("SELECT fill, step, staleness FROM t ORDER BY interpolate", + true); + assertSqlCanBeParsedAndDeparsed("SELECT interpolate FROM t WHERE fill = 1 AND step = 2", + true); + } + @Test public void testCastToNestedParametricTypeIssue2441() throws JSQLParserException { // ClickHouse allows parametric (constructor-style) data types as a CAST target,