Skip to content

Commit 4f161e3

Browse files
committed
feat(parser): support ClickHouse transformer STRICT modifier and EXCEPT regex patterns
Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent bb55bb9 commit 4f161e3

5 files changed

Lines changed: 437 additions & 23 deletions

File tree

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ public enum ColumnsTransformerType {
3131
private ColumnsTransformerType type;
3232
private Expression applyExpression;
3333
private ParenthesedExpressionList<Column> exceptColumns;
34+
private StringValue exceptPattern;
3435
private List<SelectItem<?>> replaceItems;
36+
private boolean strict;
3537

3638
public ColumnsTransformer(ColumnsTransformerType type) {
3739
this.type = type;
@@ -61,6 +63,39 @@ public ParenthesedExpressionList<Column> getExceptColumns() {
6163

6264
public ColumnsTransformer setExceptColumns(ParenthesedExpressionList<Column> exceptColumns) {
6365
this.exceptColumns = exceptColumns;
66+
if (exceptColumns != null) {
67+
this.exceptPattern = null;
68+
}
69+
return this;
70+
}
71+
72+
/**
73+
* The re2 pattern of the ClickHouse {@code EXCEPT 'pattern'} form, mutually exclusive with
74+
* {@link #getExceptColumns()}.
75+
*/
76+
public StringValue getExceptPattern() {
77+
return exceptPattern;
78+
}
79+
80+
public ColumnsTransformer setExceptPattern(StringValue exceptPattern) {
81+
this.exceptPattern = exceptPattern;
82+
if (exceptPattern != null) {
83+
this.exceptColumns = null;
84+
}
85+
return this;
86+
}
87+
88+
/**
89+
* The {@code STRICT} modifier of ClickHouse {@code EXCEPT}/{@code REPLACE} transformers: named
90+
* columns must exist, otherwise the database rejects the query. Meaningless for {@code APPLY}
91+
* and {@code EXCLUDE}.
92+
*/
93+
public boolean isStrict() {
94+
return strict;
95+
}
96+
97+
public ColumnsTransformer setStrict(boolean strict) {
98+
this.strict = strict;
6499
return this;
65100
}
66101

@@ -81,6 +116,13 @@ void collectExpressions(List<Expression> expressions) {
81116
}
82117
break;
83118
case EXCEPT:
119+
if (exceptPattern != null) {
120+
expressions.add(exceptPattern);
121+
}
122+
if (exceptColumns != null) {
123+
expressions.addAll(exceptColumns);
124+
}
125+
break;
84126
case EXCLUDE:
85127
if (exceptColumns != null) {
86128
expressions.addAll(exceptColumns);
@@ -104,13 +146,15 @@ public StringBuilder appendTo(StringBuilder builder) {
104146
builder.append("APPLY(").append(applyExpression).append(")");
105147
break;
106148
case EXCEPT:
107-
builder.append("EXCEPT ").append(exceptColumns);
149+
builder.append("EXCEPT").append(strict ? " STRICT" : "").append(" ")
150+
.append(exceptPattern != null ? exceptPattern : exceptColumns);
108151
break;
109152
case EXCLUDE:
110153
builder.append("EXCLUDE ").append(exceptColumns);
111154
break;
112155
case REPLACE:
113-
builder.append("REPLACE(").append(Select.getStringList(replaceItems)).append(")");
156+
builder.append("REPLACE").append(strict ? " STRICT" : "").append("(")
157+
.append(Select.getStringList(replaceItems)).append(")");
114158
break;
115159
default:
116160
throw new IllegalStateException("Unhandled ColumnsTransformerType: " + type);

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,11 +1870,23 @@ public <S> StringBuilder visit(ColumnsExpression columnsExpression, S context) {
18701870
builder.append(")");
18711871
break;
18721872
case EXCEPT:
1873-
builder.append(" EXCEPT ");
1874-
transformer.getExceptColumns().accept(this, context);
1873+
builder.append(" EXCEPT");
1874+
if (transformer.isStrict()) {
1875+
builder.append(" STRICT");
1876+
}
1877+
builder.append(" ");
1878+
if (transformer.getExceptPattern() != null) {
1879+
transformer.getExceptPattern().accept(this, context);
1880+
} else {
1881+
transformer.getExceptColumns().accept(this, context);
1882+
}
18751883
break;
18761884
case REPLACE:
1877-
builder.append(" REPLACE(");
1885+
builder.append(" REPLACE");
1886+
if (transformer.isStrict()) {
1887+
builder.append(" STRICT");
1888+
}
1889+
builder.append("(");
18781890
boolean first = true;
18791891
for (SelectItem<?> item : transformer.getReplaceItems()) {
18801892
if (!first) {

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8252,8 +8252,9 @@ boolean ColumnsTransformerAhead() #void:
82528252
{
82538253
LOOKAHEAD( <K_APPLY> "(" RelObjectName() ")" ) { return true; }
82548254
| LOOKAHEAD( ( <K_APPLY> | <K_REPLACE> ) AliasColumns() ) { return false; }
8255-
| LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> "("
8256-
| ( <K_EXCEPT> | <K_EXCLUDE> ) ParenthesedColumnList() )
8255+
| LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8256+
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8257+
| <K_EXCLUDE> ParenthesedColumnList() )
82578258
{ return true; }
82588259
| { return false; }
82598260
}
@@ -8271,8 +8272,9 @@ boolean AllColumnsTransformerAhead() #void:
82718272
{
82728273
LOOKAHEAD( <K_APPLY> "(" RelObjectName() ")" ) { return true; }
82738274
| LOOKAHEAD( <K_APPLY> AliasColumns() ) { return false; }
8274-
| LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> "("
8275-
| ( <K_EXCEPT> | <K_EXCLUDE> ) ParenthesedColumnList() )
8275+
| LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8276+
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8277+
| <K_EXCLUDE> ParenthesedColumnList() )
82768278
{ return true; }
82778279
| { return false; }
82788280
}
@@ -8298,7 +8300,8 @@ Expression SelectItemExpression() #ColumnsExpression(transformers != null):
82988300
( expression = Condition() { expression = prattExpressionRest(expression, 2); } )
82998301
)
83008302
(
8301-
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> "(" | <K_EXCEPT> ParenthesedColumnList() )
8303+
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8304+
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> ) )
83028305
{
83038306
if (!(expression instanceof Function)) {
83048307
break;
@@ -8330,17 +8333,19 @@ Expression SelectItemExpression() #ColumnsExpression(transformers != null):
83308333
}
83318334

83328335
/**
8333-
* Parses a single ClickHouse transformer: APPLY(expression), EXCEPT (columns) or
8334-
* REPLACE (selectItems). ClickHouse parses these in a loop, so they may repeat
8335-
* and combine in any order.
8336+
* Parses a single ClickHouse transformer: APPLY(expression), EXCEPT [STRICT] (columns)
8337+
* or a pattern, REPLACE [STRICT] (selectItems). ClickHouse parses these in a loop, so
8338+
* they may repeat and combine in any order.
83368339
*/
83378340
ColumnsTransformer ColumnsTransformer():
83388341
{
83398342
Expression applyExpression;
8340-
ParenthesedExpressionList<Column> exceptColumns;
8343+
ParenthesedExpressionList<Column> exceptColumns = null;
8344+
StringValue exceptPattern = null;
83418345
List<SelectItem<?>> replaceItems;
83428346
ColumnsTransformer transformer;
83438347
Token tk;
8348+
boolean strict;
83448349
}
83458350
{
83468351
(
@@ -8354,16 +8359,32 @@ ColumnsTransformer ColumnsTransformer():
83548359
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.APPLY)
83558360
.setApplyExpression(applyExpression); }
83568361

8357-
| ( tk=<K_EXCEPT> | tk=<K_EXCLUDE> ) exceptColumns = ParenthesedColumnList()
8358-
{ transformer = new ColumnsTransformer(
8359-
tk.kind == K_EXCLUDE
8360-
? ColumnsTransformer.ColumnsTransformerType.EXCLUDE
8361-
: ColumnsTransformer.ColumnsTransformerType.EXCEPT)
8362+
| <K_EXCEPT>
8363+
{ strict = false; }
8364+
[ <K_STRICT> { strict = true; } ]
8365+
(
8366+
exceptColumns = ParenthesedColumnList()
8367+
|
8368+
tk=<S_CHAR_LITERAL> { exceptPattern = new StringValue(tk.image); }
8369+
)
8370+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT)
8371+
.setStrict(strict);
8372+
if (exceptPattern != null) {
8373+
transformer.setExceptPattern(exceptPattern);
8374+
} else {
8375+
transformer.setExceptColumns(exceptColumns);
8376+
} }
8377+
8378+
| <K_EXCLUDE> exceptColumns = ParenthesedColumnList()
8379+
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCLUDE)
83628380
.setExceptColumns(exceptColumns); }
83638381

8364-
| <K_REPLACE> "(" replaceItems = SelectItemsList() ")"
8382+
| <K_REPLACE>
8383+
{ strict = false; }
8384+
[ <K_STRICT> { strict = true; } ]
8385+
"(" replaceItems = SelectItemsList() ")"
83658386
{ transformer = new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.REPLACE)
8366-
.setReplaceItems(replaceItems); }
8387+
.setStrict(strict).setReplaceItems(replaceItems); }
83678388
)
83688389
{ return transformer; }
83698390
}
@@ -8383,8 +8404,9 @@ AllColumns AllColumns(boolean allowAdditions):
83838404
// BigData allows EXCEPT, DuckDB allows EXCLUDE, ClickHouse allows APPLY;
83848405
// they may repeat and combine in any order
83858406
(
8386-
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> "("
8387-
| ( <K_EXCEPT> | <K_EXCLUDE> ) ParenthesedColumnList() )
8407+
LOOKAHEAD( <K_APPLY> "(" | <K_REPLACE> [ <K_STRICT> ] "("
8408+
| <K_EXCEPT> [ <K_STRICT> ] ( ParenthesedColumnList() | <S_CHAR_LITERAL> )
8409+
| <K_EXCLUDE> ParenthesedColumnList() )
83888410
{
83898411
if (!allowAdditions || !AllColumnsTransformerAhead()) {
83908412
break;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNull;
14+
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import net.sf.jsqlparser.JSQLParserException;
18+
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
19+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
20+
import net.sf.jsqlparser.schema.Column;
21+
import net.sf.jsqlparser.statement.select.AllColumns;
22+
import net.sf.jsqlparser.statement.select.PlainSelect;
23+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.ValueSource;
27+
28+
class ColumnsTransformerTest {
29+
30+
@ParameterizedTest
31+
@ValueSource(strings = {"*", "t.*", "COLUMNS('m')"})
32+
void testExceptColumnsReplacePattern(String matcher) throws JSQLParserException {
33+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
34+
"SELECT " + matcher + " EXCEPT STRICT '^tmp_' FROM t");
35+
Expression expression = select.getSelectItem(0).getExpression();
36+
ColumnsTransformer transformer = getTransformer(expression);
37+
transformer.setExceptColumns(new ParenthesedExpressionList<>(new Column("keep")));
38+
39+
assertOutput("SELECT " + matcher + " EXCEPT STRICT (keep) FROM t", select);
40+
assertNull(transformer.getExceptPattern());
41+
if (expression instanceof ColumnsExpression) {
42+
assertVisitedValues(List.of("m", "keep"), expression);
43+
}
44+
}
45+
46+
@ParameterizedTest
47+
@ValueSource(strings = {"*", "t.*", "COLUMNS('m')"})
48+
void testExceptPatternReplacesColumns(String matcher) throws JSQLParserException {
49+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
50+
"SELECT " + matcher + " EXCEPT STRICT (keep) FROM t");
51+
Expression expression = select.getSelectItem(0).getExpression();
52+
ColumnsTransformer transformer = getTransformer(expression);
53+
transformer.setExceptPattern(new StringValue("'^tmp_'"));
54+
55+
assertOutput("SELECT " + matcher + " EXCEPT STRICT '^tmp_' FROM t", select);
56+
if (expression instanceof ColumnsExpression) {
57+
assertVisitedValues(List.of("m", "^tmp_"), expression);
58+
}
59+
assertNull(transformer.getExceptColumns());
60+
}
61+
62+
@Test
63+
void testClearingPatternPreservesColumns() {
64+
ColumnsTransformer transformer =
65+
new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT)
66+
.setExceptColumns(new ParenthesedExpressionList<>(new Column("keep")))
67+
.setExceptPattern(null);
68+
assertEquals("EXCEPT (keep)", transformer.toString());
69+
}
70+
71+
@Test
72+
void testClearingColumnsPreservesPattern() {
73+
ColumnsTransformer transformer =
74+
new ColumnsTransformer(ColumnsTransformer.ColumnsTransformerType.EXCEPT)
75+
.setExceptPattern(new StringValue("'^tmp_'"))
76+
.setExceptColumns(null);
77+
assertEquals("EXCEPT '^tmp_'", transformer.toString());
78+
}
79+
80+
private static ColumnsTransformer getTransformer(Expression expression) {
81+
return expression instanceof AllColumns
82+
? ((AllColumns) expression).getTransformers().get(0)
83+
: ((ColumnsExpression) expression).getTransformers().get(0);
84+
}
85+
86+
private static void assertOutput(String expected, PlainSelect select)
87+
throws JSQLParserException {
88+
assertEquals(expected, select.toString());
89+
StatementDeParser deparser = new StatementDeParser(new StringBuilder());
90+
select.accept(deparser, null);
91+
assertEquals(expected, deparser.getBuilder().toString());
92+
assertEquals(expected, CCJSqlParserUtil.parse(expected).toString());
93+
}
94+
95+
private static void assertVisitedValues(List<String> expected, Expression expression) {
96+
List<String> visited = new ArrayList<>();
97+
expression.accept(new ExpressionVisitorAdapter<Void>() {
98+
@Override
99+
public <S> Void visit(StringValue value, S context) {
100+
visited.add(value.getValue());
101+
return null;
102+
}
103+
104+
@Override
105+
public <S> Void visit(Column column, S context) {
106+
visited.add(column.getColumnName());
107+
return null;
108+
}
109+
}, null);
110+
assertEquals(expected, visited);
111+
}
112+
}

0 commit comments

Comments
 (0)