Skip to content

Commit 06f1556

Browse files
committed
feat(parser): support ClickHouse WITH expression aliases including lambdas
WITH items gained ClickHouse's WITH <expression> AS <identifier> form next to CTEs and WITH FUNCTION, reusing the existing LambdaExpression grammar for the x -> e, (params) -> e and (x -> e) shapes, ParenthesedSelect for (SELECT ...) AS name, and a bounded token scan to tell expression aliases from the CTE shape name [(cols)] AS [NOT] MATERIALIZED (statement). The generic SelectVisitorAdapter traversal reaches the expression body of such items (subquery aliases stay on the select path, other expressions go to the expression visitor), and the SEARCH/CYCLE suffix accepted by the tolerant grammar serializes on the expression branch instead of being dropped. DATA_TYPE and DATA_TYPE(N) now start an implicit typed literal only when a trailing literal completes it, so keyword-named columns and function calls (SELECT number, double(5)) parse instead of failing. Fixes #2632 Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent dec8f5d commit 06f1556

8 files changed

Lines changed: 398 additions & 8 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,9 @@ public <S> Void visit(WithItem<?> withItem, S context) {
874874
if (body instanceof Statement) {
875875
((Statement) body).accept(analysis.statements, context);
876876
}
877+
if (withItem.getExpression() != null) {
878+
withItem.getExpression().accept(new FeatureExpressionVisitor(analysis), context);
879+
}
877880
return null;
878881
}
879882
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,19 @@ public <S> T visit(WithItem<?> withItem, S context) {
313313
return ((Statement) body).accept(statementVisitor, context);
314314
}
315315

316+
// a ClickHouse expression alias (WITH <expression> AS <identifier>): a
317+
// parenthesized subquery stays on the select path, any other expression
318+
// goes to the expression visitor
319+
Expression expression = withItem.getExpression();
320+
if (expression != null) {
321+
if (expression instanceof Select) {
322+
return ((Select) expression).accept(this, context);
323+
}
324+
if (expressionVisitor != null) {
325+
return expression.accept(expressionVisitor, context);
326+
}
327+
}
328+
316329
// no statement visitor available: skip the body rather than fail
317330
return null;
318331
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class WithItem<K extends ParenthesedStatement> implements Serializable {
3030
private Alias alias;
3131
private List<SelectItem<?>> withItemList;
3232
private WithFunctionDeclaration withFunctionDeclaration;
33+
private Expression expression;
3334
private WithSearchClause searchClause;
3435
private WithCycleClause cycleClause;
3536
private boolean recursive = false;
@@ -45,6 +46,25 @@ public WithItem() {
4546
this(null, (Alias) null);
4647
}
4748

49+
/**
50+
* The aliased expression of a WITH item like ClickHouse's
51+
* {@code WITH <expression> AS <identifier>}, where the item does not hold a statement.
52+
*
53+
* @return the expression of this WITH item, or null for statement-based (CTE) items
54+
*/
55+
public Expression getExpression() {
56+
return expression;
57+
}
58+
59+
public void setExpression(Expression expression) {
60+
this.expression = expression;
61+
}
62+
63+
public WithItem<K> withExpression(Expression expression) {
64+
this.setExpression(expression);
65+
return this;
66+
}
67+
4868
public K getParenthesedStatement() {
4969
return statement;
5070
}
@@ -184,6 +204,12 @@ public String toString() {
184204
StringBuilder builder = new StringBuilder();
185205
if (withFunctionDeclaration != null) {
186206
builder.append(withFunctionDeclaration);
207+
} else if (expression != null) {
208+
builder.append(expression);
209+
if (alias != null) {
210+
builder.append(" AS ").append(alias.getName());
211+
}
212+
appendRecursiveClausesTo(builder, expr -> builder.append(expr));
187213
} else {
188214
builder.append(recursive ? "RECURSIVE " : "");
189215
if (alias != null) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ public <S> Void visit(WithItem<?> withItem, S context) {
354354
}
355355
// dispatch any ParenthesedStatement payload (Select, Delete, Update, Insert)
356356
withItem.accept((StatementVisitor<?>) this, context);
357+
// an expression alias (WITH expr AS name) may read tables on its own
358+
if (withItem.getExpression() != null) {
359+
withItem.getExpression().accept(this, context);
360+
}
357361
return null;
358362
}
359363

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ <S> StringBuilder deparseWithItem(WithItem<?> item, S context) {
8585
if (item.getWithFunctionDeclaration() != null) {
8686
return builder.append(item.getWithFunctionDeclaration());
8787
}
88+
if (item.getExpression() != null) {
89+
item.getExpression().accept(expressions, context);
90+
if (item.getAlias() != null) {
91+
builder.append(" AS ").append(item.getAlias().getName());
92+
}
93+
return item.appendRecursiveClausesTo(builder,
94+
expression -> expression.accept(expressions, context));
95+
}
8896
if (item.isRecursive()) {
8997
builder.append("RECURSIVE ");
9098
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ public <S> Void visit(WithItem<?> withItem, S context) {
416416
if (withItem.getCycleClause() != null) {
417417
withItem.getCycleClause().accept(getValidator(ExpressionValidator.class), context);
418418
}
419+
if (withItem.getExpression() != null) {
420+
withItem.getExpression().accept(getValidator(ExpressionValidator.class), context);
421+
}
419422
withItem.accept(getValidator(StatementValidator.class), context);
420423
return null;
421424
}

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

Lines changed: 173 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,119 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
711711
}
712712
}
713713

714+
/** The literal tokens that may complete a typed literal in {@link #isImplicitCastAhead()}. */
715+
private boolean isTypedLiteralFollower(int kind) {
716+
return kind == S_CHAR_LITERAL || kind == S_LONG || kind == S_DOUBLE;
717+
}
718+
719+
/**
720+
* Whether the token could start a {@link #RelObjectName()}, mirroring the token
721+
* alternatives accepted there (base identifiers, DATA_TYPE, non-reserved keywords
722+
* and the reserved-keyword-as-identifier list).
723+
*/
724+
private static boolean isIdentifierishNameToken(Token t) {
725+
switch (t.kind) {
726+
case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case DATA_TYPE:
727+
case K_DATETIMELITERAL: case K_DATE_LITERAL:
728+
case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT: case K_CREATE:
729+
case K_DEFAULT: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF:
730+
case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: case K_LEFT:
731+
case K_LIMIT: case K_NEXTVAL: case K_OFFSET: case K_ON: case K_OPTIMIZE:
732+
case K_ORDER: case K_PROCEDURE: case K_PUBLIC: case K_QUALIFY: case K_RIGHT:
733+
case K_FILE: case K_SET: case K_SOME: case K_START: case K_TABLES:
734+
case K_TOP: case K_VALUE: case K_VALUES:
735+
return true;
736+
default:
737+
return t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD;
738+
}
739+
}
740+
741+
/**
742+
* Skips "." name continuations starting at token position {@code i}; returns the
743+
* position of the first token after the qualified name.
744+
*/
745+
private int skipQualifiedObjectName(int i) {
746+
int guard = 0;
747+
while (guard++ < 64 && ".".equals(getToken(i).image)) {
748+
i += 2;
749+
}
750+
return i;
751+
}
752+
753+
/**
754+
* Skips the balanced round-bracket group opening at token position {@code i};
755+
* returns the position after the matching closing bracket, or -1 when the
756+
* group is unbalanced or the scan would run past the token stream.
757+
*/
758+
private int skipBalancedBracketGroup(int i) {
759+
int depth = 0;
760+
int guard = 0;
761+
while (guard++ < 4096) {
762+
int kind = getToken(i).kind;
763+
if (kind == 0) {
764+
return -1;
765+
}
766+
if (kind == OPENING_BRACKET) {
767+
depth++;
768+
} else if (kind == CLOSING_BRACKET) {
769+
depth--;
770+
if (depth == 0) {
771+
return i + 1;
772+
}
773+
}
774+
i++;
775+
}
776+
return -1;
777+
}
778+
779+
/**
780+
* True when the upcoming WITH item is ClickHouse's expression alias form
781+
* {@code WITH <expression> AS <identifier>} (including lambda aliases such as
782+
* {@code x -> x * 2} or {@code (x -> x * 2)}), and false for the classic CTE
783+
* shape {@code name [(cols)] AS [NOT] MATERIALIZED (statement)} or for the
784+
* other WithItem alternatives (FUNCTION, RECURSIVE).
785+
*
786+
* <p>Everything that cannot start a CTE name (literals, brackets, operators,
787+
* function calls) can only be an expression. A name-shaped head followed by
788+
* anything but an AS decision point also continues as an expression
789+
* ({@code total * 2}, {@code case when .. end}); the CTE shape is exactly
790+
* the name-shaped head with AS before a parenthesized statement.</p>
791+
*/
792+
private boolean isWithExpressionAliasAhead() {
793+
try {
794+
Token t1 = getToken(1);
795+
if (t1.kind == K_FUNCTION || t1.kind == K_RECURSIVE) {
796+
return false;
797+
}
798+
if (!isIdentifierishNameToken(t1)) {
799+
return true;
800+
}
801+
int i = skipQualifiedObjectName(2);
802+
if (getToken(i).kind == OPENING_BRACKET) {
803+
// a column list (CTE) or call arguments (expression); the decision
804+
// point sits behind the balanced group either way
805+
i = skipBalancedBracketGroup(i);
806+
if (i < 0) {
807+
return false;
808+
}
809+
}
810+
if (getToken(i).kind != K_AS) {
811+
return true;
812+
}
813+
i++;
814+
if (getToken(i).kind == K_NOT) {
815+
i++;
816+
}
817+
if (getToken(i).kind == K_MATERIALIZED) {
818+
i++;
819+
}
820+
// the CTE shape requires a parenthesized statement here
821+
return getToken(i).kind != OPENING_BRACKET;
822+
} catch (TokenMgrException e) {
823+
return false;
824+
}
825+
}
826+
714827
protected boolean isImplicitCastAhead() {
715828
try {
716829
int k1 = getToken(1).kind;
@@ -721,18 +834,33 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
721834
if (k1 == K_JSON) return getToken(2).kind == S_CHAR_LITERAL;
722835
if (k1 != DATA_TYPE) return false;
723836
int k2 = getToken(2).kind;
724-
if (k2 != OPENING_BRACKET) return true; // DATA_TYPE literal - simple cast
725-
// DATA_TYPE( ... ) - precision cast if content is only S_LONG literals
726-
// function call otherwise (e.g. UUID(), VARCHAR(col))
837+
if (k2 != OPENING_BRACKET) {
838+
// a bare DATA_TYPE is a typed literal (INT '5') only when a trailing
839+
// literal completes it, possibly after further type-name tokens
840+
// (DOUBLE PRECISION '1'); otherwise it is an identifier, e.g. a
841+
// column named number or a keyword-named lambda alias
842+
if (isTypedLiteralFollower(k2)) {
843+
return true;
844+
}
845+
return k2 == DATA_TYPE && isTypedLiteralFollower(getToken(3).kind);
846+
}
847+
// DATA_TYPE( ... ) - precision cast only when a trailing literal completes
848+
// the typed literal (DATA_TYPE(N) 'lit'); a function call otherwise,
849+
// e.g. double(5), int(5), VARCHAR(col), UUID()
727850
int k3 = getToken(3).kind;
728851
if (k3 == CLOSING_BRACKET) return false; // DATA_TYPE() - empty call
729852
if (k3 != S_LONG) return false; // DATA_TYPE(expr) - function call
730853
int k4 = getToken(4).kind;
731-
if (k4 == CLOSING_BRACKET) return true; // DATA_TYPE(N) - precision cast
732-
if (k4 != K_COMMA) return false; // DATA_TYPE(N expr) - function call
733-
int k5 = getToken(5).kind;
734-
if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call
735-
return getToken(6).kind == CLOSING_BRACKET; // DATA_TYPE(N,M) - precision cast
854+
if (k4 == K_COMMA) {
855+
int k5 = getToken(5).kind;
856+
if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call
857+
if (getToken(6).kind != CLOSING_BRACKET) {
858+
return false; // DATA_TYPE(N,M expr) - function call
859+
}
860+
return isTypedLiteralFollower(getToken(7).kind); // DATA_TYPE(N,M) 'lit'
861+
}
862+
if (k4 != CLOSING_BRACKET) return false; // DATA_TYPE(N expr) - function call
863+
return isTypedLiteralFollower(getToken(5).kind); // DATA_TYPE(N) 'lit'
736864
} catch (TokenMgrException e) {
737865
return false;
738866
}
@@ -7011,6 +7139,7 @@ WithItem<?> WithItem() #WithItem:
70117139
List<SelectItem<?>> selectItems = null;
70127140
WithFunctionDeclaration withFunctionDeclaration = null;
70137141
ParenthesedStatement statement = null;
7142+
Expression expr = null;
70147143
WithSearchClause withSearchClause = null;
70157144
WithCycleClause withCycleClause = null;
70167145
WithItem<?> withItem;
@@ -7023,6 +7152,7 @@ WithItem<?> WithItem() #WithItem:
70237152
withItem = new WithItem().withWithFunctionDeclaration(withFunctionDeclaration);
70247153
}
70257154
|
7155+
LOOKAHEAD({ !isWithExpressionAliasAhead() })
70267156
(
70277157
[ LOOKAHEAD(2) <K_RECURSIVE> { recursive = true; } ]
70287158
name=RelObjectName()
@@ -7044,6 +7174,16 @@ WithItem<?> WithItem() #WithItem:
70447174
.withWithItemList(selectItems);
70457175
}
70467176
)
7177+
|
7178+
(
7179+
// ClickHouse style expression alias: WITH <expression> AS <identifier>
7180+
expr = WithItemExpression()
7181+
<K_AS>
7182+
name = RelObjectName()
7183+
{
7184+
withItem = new WithItem().withExpression(expr).withAlias(new Alias(name, false));
7185+
}
7186+
)
70477187
)
70487188
[ withSearchClause = WithSearchClause() { withItem.setSearchClause(withSearchClause); } ]
70497189
[ withCycleClause = WithCycleClause() { withItem.setCycleClause(withCycleClause); } ]
@@ -7052,6 +7192,31 @@ WithItem<?> WithItem() #WithItem:
70527192
}
70537193
}
70547194

7195+
Expression WithItemExpression():
7196+
{
7197+
LambdaExpression lambdaExpression;
7198+
Expression expr;
7199+
}
7200+
{
7201+
(
7202+
// unparenthesized parameter, e.g. x -> x * 2
7203+
LOOKAHEAD( RelObjectName() "->" )
7204+
expr = LambdaExpression()
7205+
|
7206+
// parenthesized parameter list, e.g. (value) -> value + 1
7207+
LOOKAHEAD( ParenthesedColumnList() "->" )
7208+
expr = LambdaExpression()
7209+
|
7210+
// whole lambda wrapped in parentheses, e.g. (x -> x * 2)
7211+
LOOKAHEAD( "(" ( RelObjectName() | ParenthesedColumnList() ) "->" )
7212+
"(" lambdaExpression = LambdaExpression() ")"
7213+
{ expr = new ParenthesedExpressionList<Expression>(lambdaExpression); }
7214+
|
7215+
expr = Expression()
7216+
)
7217+
{ return expr; }
7218+
}
7219+
70557220
WithSearchClause WithSearchClause() #WithSearchClause:
70567221
{
70577222
Token orderingToken;

0 commit comments

Comments
 (0)