Skip to content

Commit 406a4d4

Browse files
authored
fix: support ClickHouse ORDER BY WITH FILL and INTERPOLATE (#2467) (#2469)
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: 付典 <fudianchn@gmail.com>
1 parent 60b0875 commit 406a4d4

10 files changed

Lines changed: 446 additions & 1 deletion

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import java.io.Serializable;
13+
14+
import net.sf.jsqlparser.expression.Expression;
15+
import net.sf.jsqlparser.schema.Column;
16+
17+
/**
18+
* Models one column entry of the ClickHouse {@code INTERPOLATE} clause, which computes the values
19+
* of non-sorted columns for the rows generated by {@code ORDER BY ... WITH FILL} (see
20+
* <a href="https://clickhouse.com/docs/en/sql-reference/statements/select/order-by">ClickHouse:
21+
* ORDER BY</a>).
22+
* <p>
23+
* {@code col AS expr} evaluates {@code expr} for the generated rows, while a bare {@code col} (a
24+
* {@code null} expression) repeats the previous value of the column.
25+
*/
26+
public class InterpolateElement implements Serializable {
27+
28+
private Column column;
29+
private Expression expression;
30+
31+
public InterpolateElement() {
32+
super();
33+
}
34+
35+
public InterpolateElement(Column column, Expression expression) {
36+
this.column = column;
37+
this.expression = expression;
38+
}
39+
40+
public Column getColumn() {
41+
return column;
42+
}
43+
44+
public void setColumn(Column column) {
45+
this.column = column;
46+
}
47+
48+
public InterpolateElement withColumn(Column column) {
49+
setColumn(column);
50+
return this;
51+
}
52+
53+
public Expression getExpression() {
54+
return expression;
55+
}
56+
57+
public void setExpression(Expression expression) {
58+
this.expression = expression;
59+
}
60+
61+
public InterpolateElement withExpression(Expression expression) {
62+
setExpression(expression);
63+
return this;
64+
}
65+
66+
@Override
67+
public String toString() {
68+
if (expression != null) {
69+
return column + " AS " + expression;
70+
}
71+
return column.toString();
72+
}
73+
}

src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class OrderByElement implements Serializable {
2222
private boolean asc = true;
2323
private boolean ascDescPresent = false;
2424
private NullOrdering nullOrdering;
25+
private WithFill withFill;
2526

2627
public boolean isAsc() {
2728
return asc;
@@ -39,6 +40,20 @@ public void setNullOrdering(NullOrdering nullOrdering) {
3940
this.nullOrdering = nullOrdering;
4041
}
4142

43+
public WithFill getWithFill() {
44+
return withFill;
45+
}
46+
47+
public OrderByElement setWithFill(WithFill withFill) {
48+
this.withFill = withFill;
49+
return this;
50+
}
51+
52+
public OrderByElement withWithFill(WithFill withFill) {
53+
setWithFill(withFill);
54+
return this;
55+
}
56+
4257
public boolean isAscDescPresent() {
4358
return ascDescPresent;
4459
}
@@ -74,6 +89,9 @@ public String toString() {
7489
b.append(' ');
7590
b.append(nullOrdering == NullOrdering.NULLS_FIRST ? "NULLS FIRST" : "NULLS LAST");
7691
}
92+
if (withFill != null) {
93+
b.append(' ').append(withFill);
94+
}
7795
if (isMysqlWithRollup()) {
7896
b.append(" WITH ROLLUP");
7997
}

src/main/java/net/sf/jsqlparser/statement/select/Select.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp
3636
ForClause forClause = null;
3737

3838
List<OrderByElement> orderByElements;
39+
List<InterpolateElement> interpolate;
3940
ForMode forMode = null;
4041
private boolean skipLocked;
4142
private Wait wait;
@@ -54,6 +55,16 @@ public static String orderByToString(boolean oracleSiblings,
5455
return getFormattedList(orderByElements, oracleSiblings ? "ORDER SIBLINGS BY" : "ORDER BY");
5556
}
5657

58+
public static String interpolateToString(List<InterpolateElement> interpolate) {
59+
if (interpolate == null) {
60+
return "";
61+
}
62+
if (interpolate.isEmpty()) {
63+
return " INTERPOLATE";
64+
}
65+
return getFormattedList(interpolate, "INTERPOLATE", true, true);
66+
}
67+
5768
public static String getFormattedList(List<?> list, String expression) {
5869
return getFormattedList(list, expression, true, false);
5970
}
@@ -192,6 +203,19 @@ public void setOrderByElements(List<OrderByElement> orderByElements) {
192203
this.orderByElements = orderByElements;
193204
}
194205

206+
public List<InterpolateElement> getInterpolate() {
207+
return interpolate;
208+
}
209+
210+
public void setInterpolate(List<InterpolateElement> interpolate) {
211+
this.interpolate = interpolate;
212+
}
213+
214+
public Select withInterpolate(List<InterpolateElement> interpolate) {
215+
setInterpolate(interpolate);
216+
return this;
217+
}
218+
195219
public Select withOrderByElements(List<OrderByElement> orderByElements) {
196220
this.setOrderByElements(orderByElements);
197221
return this;
@@ -463,6 +487,7 @@ public StringBuilder appendTo(StringBuilder builder) {
463487

464488
if (!forUpdateBeforeOrderBy) {
465489
builder.append(orderByToString(oracleSiblings, orderByElements));
490+
builder.append(interpolateToString(interpolate));
466491
}
467492

468493
if (forClause != null) {
@@ -512,6 +537,7 @@ public StringBuilder appendTo(StringBuilder builder) {
512537

513538
if (forUpdateBeforeOrderBy) {
514539
builder.append(orderByToString(oracleSiblings, orderByElements));
540+
builder.append(interpolateToString(interpolate));
515541
}
516542

517543
return builder;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import java.io.Serializable;
13+
14+
import net.sf.jsqlparser.expression.Expression;
15+
16+
/**
17+
* Models the ClickHouse {@code WITH FILL} modifier of an {@link OrderByElement}, which fills gaps
18+
* in the sorted sequence of the order by expression (see
19+
* <a href="https://clickhouse.com/docs/en/sql-reference/statements/select/order-by">ClickHouse:
20+
* ORDER BY</a>).
21+
* <p>
22+
* The modifier takes optional {@code FROM}, {@code TO}, {@code STEP} and {@code STALENESS} bounds,
23+
* where the step may be an {@code INTERVAL} literal. Its presence on an {@link OrderByElement}
24+
* marks the element as filled.
25+
*/
26+
public class WithFill implements Serializable {
27+
28+
private Expression from;
29+
private Expression to;
30+
private Expression step;
31+
private Expression staleness;
32+
33+
public Expression getFrom() {
34+
return from;
35+
}
36+
37+
public void setFrom(Expression from) {
38+
this.from = from;
39+
}
40+
41+
public WithFill withFrom(Expression from) {
42+
setFrom(from);
43+
return this;
44+
}
45+
46+
public Expression getTo() {
47+
return to;
48+
}
49+
50+
public void setTo(Expression to) {
51+
this.to = to;
52+
}
53+
54+
public WithFill withTo(Expression to) {
55+
setTo(to);
56+
return this;
57+
}
58+
59+
public Expression getStep() {
60+
return step;
61+
}
62+
63+
public void setStep(Expression step) {
64+
this.step = step;
65+
}
66+
67+
public WithFill withStep(Expression step) {
68+
setStep(step);
69+
return this;
70+
}
71+
72+
public Expression getStaleness() {
73+
return staleness;
74+
}
75+
76+
public void setStaleness(Expression staleness) {
77+
this.staleness = staleness;
78+
}
79+
80+
public WithFill withStaleness(Expression staleness) {
81+
setStaleness(staleness);
82+
return this;
83+
}
84+
85+
@Override
86+
public String toString() {
87+
StringBuilder b = new StringBuilder("WITH FILL");
88+
if (from != null) {
89+
b.append(" FROM ").append(from);
90+
}
91+
if (to != null) {
92+
b.append(" TO ").append(to);
93+
}
94+
if (step != null) {
95+
b.append(" STEP ").append(step);
96+
}
97+
if (staleness != null) {
98+
b.append(" STALENESS ").append(staleness);
99+
}
100+
return b.toString();
101+
}
102+
}

src/main/java/net/sf/jsqlparser/util/deparser/OrderByDeParser.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import net.sf.jsqlparser.expression.ExpressionVisitor;
1616
import net.sf.jsqlparser.statement.select.OrderByElement;
17+
import net.sf.jsqlparser.statement.select.WithFill;
1718

1819
public class OrderByDeParser extends AbstractDeParser<List<OrderByElement>> {
1920

@@ -64,11 +65,35 @@ public void deParseElement(OrderByElement orderBy) {
6465
? "NULLS FIRST"
6566
: "NULLS LAST");
6667
}
68+
if (orderBy.getWithFill() != null) {
69+
builder.append(' ');
70+
deParseWithFill(orderBy.getWithFill());
71+
}
6772
if (orderBy.isMysqlWithRollup()) {
6873
builder.append(" WITH ROLLUP");
6974
}
7075
}
7176

77+
private void deParseWithFill(WithFill withFill) {
78+
builder.append("WITH FILL");
79+
if (withFill.getFrom() != null) {
80+
builder.append(" FROM ");
81+
withFill.getFrom().accept(expressionVisitor, null);
82+
}
83+
if (withFill.getTo() != null) {
84+
builder.append(" TO ");
85+
withFill.getTo().accept(expressionVisitor, null);
86+
}
87+
if (withFill.getStep() != null) {
88+
builder.append(" STEP ");
89+
withFill.getStep().accept(expressionVisitor, null);
90+
}
91+
if (withFill.getStaleness() != null) {
92+
builder.append(" STALENESS ");
93+
withFill.getStaleness().accept(expressionVisitor, null);
94+
}
95+
}
96+
7297
void setExpressionVisitor(ExpressionVisitor<StringBuilder> expressionVisitor) {
7398
this.expressionVisitor = expressionVisitor;
7499
}

src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import net.sf.jsqlparser.statement.select.First;
5252
import net.sf.jsqlparser.statement.select.FromItem;
5353
import net.sf.jsqlparser.statement.select.FromItemVisitor;
54+
import net.sf.jsqlparser.statement.select.InterpolateElement;
5455
import net.sf.jsqlparser.statement.select.Join;
5556
import net.sf.jsqlparser.statement.select.LateralSubSelect;
5657
import net.sf.jsqlparser.statement.select.LateralView;
@@ -136,6 +137,7 @@ public <S> StringBuilder visit(ParenthesedSelect select, S context) {
136137
if (select.getOrderByElements() != null) {
137138
new OrderByDeParser(expressionVisitor, builder).deParse(select.isOracleSiblings(),
138139
select.getOrderByElements());
140+
deParseInterpolate(select.getInterpolate());
139141
}
140142

141143
Alias alias = select.getAlias();
@@ -471,9 +473,33 @@ protected void deparseOrderByElementsClause(PlainSelect plainSelect,
471473
if (orderByElements != null) {
472474
new OrderByDeParser(expressionVisitor, builder).deParse(plainSelect.isOracleSiblings(),
473475
orderByElements);
476+
deParseInterpolate(plainSelect.getInterpolate());
474477
}
475478
}
476479

480+
private void deParseInterpolate(List<InterpolateElement> interpolate) {
481+
if (interpolate == null) {
482+
return;
483+
}
484+
if (interpolate.isEmpty()) {
485+
builder.append(" INTERPOLATE");
486+
return;
487+
}
488+
builder.append(" INTERPOLATE (");
489+
for (Iterator<InterpolateElement> iter = interpolate.iterator(); iter.hasNext();) {
490+
InterpolateElement element = iter.next();
491+
element.getColumn().accept(expressionVisitor, null);
492+
if (element.getExpression() != null) {
493+
builder.append(" AS ");
494+
element.getExpression().accept(expressionVisitor, null);
495+
}
496+
if (iter.hasNext()) {
497+
builder.append(", ");
498+
}
499+
}
500+
builder.append(")");
501+
}
502+
477503
@Override
478504
public <S> StringBuilder visit(SelectItem<?> selectItem, S context) {
479505
selectItem.getExpression().accept(expressionVisitor, context);
@@ -729,6 +755,7 @@ public <S> StringBuilder visit(SetOperationList list, S context) {
729755
}
730756
if (list.getOrderByElements() != null) {
731757
new OrderByDeParser(expressionVisitor, builder).deParse(list.getOrderByElements());
758+
deParseInterpolate(list.getInterpolate());
732759
}
733760

734761
if (list.getLimit() != null) {

src/main/java/net/sf/jsqlparser/util/validation/validator/OrderByValidator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public <S> Void visit(OrderByElement orderBy, S context) {
3232
validateOptionalFeature(c, orderBy.getNullOrdering(), Feature.orderByNullOrdering);
3333
}
3434
getValidator(ExpressionValidator.class).validate(orderBy.getExpression());
35+
if (orderBy.getWithFill() != null) {
36+
validateOptionalExpression(orderBy.getWithFill().getFrom());
37+
validateOptionalExpression(orderBy.getWithFill().getTo());
38+
validateOptionalExpression(orderBy.getWithFill().getStep());
39+
validateOptionalExpression(orderBy.getWithFill().getStaleness());
40+
}
3541
return null;
3642
}
3743

0 commit comments

Comments
 (0)