From 74c9a4c99a7df02836b988050da9b5ca6a406e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Sat, 15 Aug 2026 12:49:31 +0800 Subject: [PATCH] fix: support ClickHouse ORDER BY WITH FILL and INTERPOLATE (#2467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse the ClickHouse WITH FILL modifier on ORDER BY elements together with the query-level INTERPOLATE clause, matching the upstream grammar: per element [WITH FILL] [FROM e] [TO e] [STEP e] [STALENESS e] (the step may be an INTERVAL literal), and after the ORDER BY list an optional INTERPOLATE [(col [AS expr], ...)] where the bare form fills all allowed columns. OrderByElement carries a new WithFill holder and Select (shared by PlainSelect and SetOperationList, with union hoisting) carries the interpolate column list. Rendering goes through both toString and the deparser, and the validators walk the new expressions. The keywords FILL, STEP, STALENESS and INTERPOLATE are declared non-reserved, so they remain usable as plain column names. Signed-off-by: 付典 --- .../statement/select/InterpolateElement.java | 73 +++++++++++++ .../statement/select/OrderByElement.java | 18 ++++ .../jsqlparser/statement/select/Select.java | 26 +++++ .../jsqlparser/statement/select/WithFill.java | 102 ++++++++++++++++++ .../util/deparser/OrderByDeParser.java | 25 +++++ .../util/deparser/SelectDeParser.java | 27 +++++ .../validator/OrderByValidator.java | 6 ++ .../validation/validator/SelectValidator.java | 12 +++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 59 +++++++++- .../statement/select/ClickHouseTest.java | 99 +++++++++++++++++ 10 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/InterpolateElement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/WithFill.java 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,