Skip to content

Commit 6f1ae6d

Browse files
committed
feat(parser): support ClickHouse COLUMNS(...) APPLY/EXCEPT/REPLACE transformers
Adds a dedicated select-item branch for a COLUMNS('regexp') matcher followed by one or more transformers, fixing #2631 (APPLY with a lambda such as x -> round(x, 2) failed to parse; without a lambda, APPLY(...) was silently mis-parsed as the select item alias). - new ColumnsExpression / ColumnsTransformer AST nodes with an ordered transformer list, since ClickHouse parses transformers in a loop (they may repeat and combine in any order) - semantic lookahead isColumnsExpressionAhead() keeps a plain COLUMNS('regexp') on the regular Function path - expression visitor, adapter, deparser, validator and TablesNamesFinder traverse the new node Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 537f3a6 commit 6f1ae6d

9 files changed

Lines changed: 572 additions & 0 deletions

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 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
15+
16+
/**
17+
* A ClickHouse {@code COLUMNS('regexp')} matcher with one or more transformers, for example
18+
* {@code COLUMNS('^metric_') APPLY(x -> round(x, 2))}.
19+
*
20+
* Without a transformer, {@code COLUMNS('regexp')} keeps parsing as a regular
21+
* {@link net.sf.jsqlparser.expression.Function}.
22+
*/
23+
public class ColumnsExpression extends ASTNodeAccessImpl implements Expression {
24+
25+
private Expression columns;
26+
private List<ColumnsTransformer> transformers;
27+
28+
public ColumnsExpression(Expression columns) {
29+
this.columns = columns;
30+
this.transformers = new ArrayList<>();
31+
}
32+
33+
public ColumnsExpression(Expression columns, List<ColumnsTransformer> transformers) {
34+
this.columns = columns;
35+
this.transformers = transformers;
36+
}
37+
38+
public Expression getColumns() {
39+
return columns;
40+
}
41+
42+
public ColumnsExpression setColumns(Expression columns) {
43+
this.columns = columns;
44+
return this;
45+
}
46+
47+
public List<ColumnsTransformer> getTransformers() {
48+
return transformers;
49+
}
50+
51+
public ColumnsExpression setTransformers(List<ColumnsTransformer> transformers) {
52+
this.transformers = transformers;
53+
return this;
54+
}
55+
56+
public StringBuilder appendTo(StringBuilder builder) {
57+
builder.append(columns);
58+
for (ColumnsTransformer transformer : transformers) {
59+
builder.append(" ").append(transformer);
60+
}
61+
return builder;
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return appendTo(new StringBuilder()).toString();
67+
}
68+
69+
@Override
70+
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
71+
return expressionVisitor.visit(this, context);
72+
}
73+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression;
11+
12+
import java.util.List;
13+
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
14+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
15+
import net.sf.jsqlparser.schema.Column;
16+
import net.sf.jsqlparser.statement.select.Select;
17+
import net.sf.jsqlparser.statement.select.SelectItem;
18+
19+
/**
20+
* A ClickHouse transformer following a {@code COLUMNS(...)} matcher, for example the
21+
* {@code APPLY(x -> round(x, 2))} in {@code SELECT COLUMNS('^m') APPLY(x -> round(x, 2))}.
22+
*
23+
* ClickHouse parses its transformers in a loop, so they may repeat and combine in any order.
24+
*/
25+
public class ColumnsTransformer extends ASTNodeAccessImpl {
26+
27+
public enum ColumnsTransformerType {
28+
APPLY, EXCEPT, REPLACE
29+
}
30+
31+
private ColumnsTransformerType type;
32+
private Expression applyExpression;
33+
private ParenthesedExpressionList<Column> exceptColumns;
34+
private List<SelectItem<?>> replaceItems;
35+
36+
public ColumnsTransformer(ColumnsTransformerType type) {
37+
this.type = type;
38+
}
39+
40+
public ColumnsTransformerType getType() {
41+
return type;
42+
}
43+
44+
public ColumnsTransformer setType(ColumnsTransformerType type) {
45+
this.type = type;
46+
return this;
47+
}
48+
49+
public Expression getApplyExpression() {
50+
return applyExpression;
51+
}
52+
53+
public ColumnsTransformer setApplyExpression(Expression applyExpression) {
54+
this.applyExpression = applyExpression;
55+
return this;
56+
}
57+
58+
public ParenthesedExpressionList<Column> getExceptColumns() {
59+
return exceptColumns;
60+
}
61+
62+
public ColumnsTransformer setExceptColumns(ParenthesedExpressionList<Column> exceptColumns) {
63+
this.exceptColumns = exceptColumns;
64+
return this;
65+
}
66+
67+
public List<SelectItem<?>> getReplaceItems() {
68+
return replaceItems;
69+
}
70+
71+
public ColumnsTransformer setReplaceItems(List<SelectItem<?>> replaceItems) {
72+
this.replaceItems = replaceItems;
73+
return this;
74+
}
75+
76+
public StringBuilder appendTo(StringBuilder builder) {
77+
switch (type) {
78+
case APPLY:
79+
builder.append("APPLY(").append(applyExpression).append(")");
80+
break;
81+
case EXCEPT:
82+
builder.append("EXCEPT ").append(exceptColumns);
83+
break;
84+
case REPLACE:
85+
builder.append("REPLACE(").append(Select.getStringList(replaceItems)).append(")");
86+
break;
87+
}
88+
return builder;
89+
}
90+
91+
@Override
92+
public String toString() {
93+
return appendTo(new StringBuilder()).toString();
94+
}
95+
}

‎src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,12 @@ default void visit(LambdaExpression lambdaExpression) {
796796
this.visit(lambdaExpression, null);
797797
}
798798

799+
<S> T visit(ColumnsExpression columnsExpression, S context);
800+
801+
default void visit(ColumnsExpression columnsExpression) {
802+
this.visit(columnsExpression, null);
803+
}
804+
799805
<S> T visit(HighExpression highExpression, S context);
800806

801807
default void visit(HighExpression highExpression) {

‎src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,26 @@ public <S> T visit(LambdaExpression lambdaExpression, S context) {
875875
return lambdaExpression.getExpression().accept(this, context);
876876
}
877877

878+
@Override
879+
public <S> T visit(ColumnsExpression columnsExpression, S context) {
880+
if (columnsExpression.getColumns() != null) {
881+
columnsExpression.getColumns().accept(this, context);
882+
}
883+
for (ColumnsTransformer transformer : columnsExpression.getTransformers()) {
884+
if (transformer.getApplyExpression() != null) {
885+
transformer.getApplyExpression().accept(this, context);
886+
}
887+
if (transformer.getReplaceItems() != null) {
888+
for (SelectItem<?> selectItem : transformer.getReplaceItems()) {
889+
if (selectItem.getExpression() != null) {
890+
selectItem.getExpression().accept(this, context);
891+
}
892+
}
893+
}
894+
}
895+
return null;
896+
}
897+
878898
@Override
879899
public <S> T visit(HighExpression highExpression, S context) {
880900
return highExpression.getExpression().accept(this, context);

‎src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,6 +2227,26 @@ public <S> Void visit(LambdaExpression lambdaExpression, S context) {
22272227
return null;
22282228
}
22292229

2230+
@Override
2231+
public <S> Void visit(ColumnsExpression columnsExpression, S context) {
2232+
if (columnsExpression.getColumns() != null) {
2233+
columnsExpression.getColumns().accept(this, context);
2234+
}
2235+
for (ColumnsTransformer transformer : columnsExpression.getTransformers()) {
2236+
if (transformer.getApplyExpression() != null) {
2237+
transformer.getApplyExpression().accept(this, context);
2238+
}
2239+
if (transformer.getReplaceItems() != null) {
2240+
for (SelectItem<?> selectItem : transformer.getReplaceItems()) {
2241+
if (selectItem.getExpression() != null) {
2242+
selectItem.getExpression().accept(this, context);
2243+
}
2244+
}
2245+
}
2246+
}
2247+
return null;
2248+
}
2249+
22302250
@Override
22312251
public <S> Void visit(HighExpression highExpression, S context) {
22322252
highExpression.getExpression().accept(this, context);

‎src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import net.sf.jsqlparser.expression.CaseExpression;
2626
import net.sf.jsqlparser.expression.CastExpression;
2727
import net.sf.jsqlparser.expression.CollateExpression;
28+
import net.sf.jsqlparser.expression.ColumnsExpression;
2829
import net.sf.jsqlparser.expression.ConnectByPriorOperator;
2930
import net.sf.jsqlparser.expression.ConnectByRootOperator;
3031
import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
@@ -1926,6 +1927,11 @@ public <S> StringBuilder visit(LambdaExpression lambdaExpression, S context) {
19261927
return builder;
19271928
}
19281929

1930+
@Override
1931+
public <S> StringBuilder visit(ColumnsExpression columnsExpression, S context) {
1932+
return columnsExpression.appendTo(builder);
1933+
}
1934+
19291935
@Override
19301936
public <S> StringBuilder visit(HighExpression highExpression, S context) {
19311937
return builder.append(highExpression.toString());

‎src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import net.sf.jsqlparser.expression.CaseExpression;
2121
import net.sf.jsqlparser.expression.CastExpression;
2222
import net.sf.jsqlparser.expression.CollateExpression;
23+
import net.sf.jsqlparser.expression.ColumnsExpression;
24+
import net.sf.jsqlparser.expression.ColumnsTransformer;
2325
import net.sf.jsqlparser.expression.ConnectByPriorOperator;
2426
import net.sf.jsqlparser.expression.ConnectByRootOperator;
2527
import net.sf.jsqlparser.expression.DateTimeLiteralExpression;
@@ -1218,6 +1220,26 @@ public <S> Void visit(LambdaExpression lambdaExpression, S context) {
12181220
return null;
12191221
}
12201222

1223+
@Override
1224+
public <S> Void visit(ColumnsExpression columnsExpression, S context) {
1225+
if (columnsExpression.getColumns() != null) {
1226+
columnsExpression.getColumns().accept(this, context);
1227+
}
1228+
for (ColumnsTransformer transformer : columnsExpression.getTransformers()) {
1229+
if (transformer.getApplyExpression() != null) {
1230+
transformer.getApplyExpression().accept(this, context);
1231+
}
1232+
if (transformer.getReplaceItems() != null) {
1233+
for (SelectItem<?> selectItem : transformer.getReplaceItems()) {
1234+
if (selectItem.getExpression() != null) {
1235+
selectItem.getExpression().accept(this, context);
1236+
}
1237+
}
1238+
}
1239+
}
1240+
return null;
1241+
}
1242+
12211243
@Override
12221244
public <S> Void visit(HighExpression highExpression, S context) {
12231245
highExpression.getExpression().accept(this, context);

0 commit comments

Comments
 (0)