Skip to content

feat: support ClickHouse WITH expression aliases including lambdas - #2644

Merged
manticore-projects merged 3 commits into
JSQLParser:masterfrom
fudianchn:feat/clickhouse-with-expression-alias-2632
Sep 17, 2026
Merged

manticore-projects merged 3 commits into
JSQLParser:masterfrom
fudianchn:feat/clickhouse-with-expression-alias-2632

Conversation

@fudianchn

@fudianchn fudianchn commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

AI disclosure: this change was prepared with AI coding agents.

What

WITH items accept ClickHouse's WITH <expression> AS <identifier> next to classic CTEs and WITH FUNCTION: scalar, column, and compound expressions, the three lambda shapes ClickHouse accepts (x -> e, (x, y) -> e, (x -> e)), and the reversed-CTE reading (SELECT ...) AS name. Parsing reuses the existing LambdaExpression grammar; the whole-lambda-in-parentheses form is modeled as a ParenthesedExpressionList, matching how (x -> e) already parses in select-item position. WithItem exposes getExpression().

Why

The query from #2632 fails at the first WITH item on master, and with it every documented WITH <expr> AS name form, including the lambda examples in the ClickHouse documentation.

How

  1. WithItem() gains a third alternative WithItemExpression(), following the WithFunctionDeclaration precedent (Fix[2306] - Adds support for Trino UDF #2307). Lambda bodies reuse the existing LambdaExpression production.
  2. isWithExpressionAliasAhead() distinguishes expression aliases from the CTE shape name [(cols)] AS [NOT] MATERIALIZED (statement). It walks the optional bracket group through the token chain until the matching closing bracket or EOF, and aborts on parser interruption. There is no token-count limit. Qualified names go directly to the expression branch because a CTE name is a single identifier token.
  3. isImplicitCastAhead() no longer treats every DATA_TYPE / DATA_TYPE(N) as a typed-literal prefix: it starts one only when a trailing literal completes it, otherwise the tokens parse as an identifier or function call. INT '5', DECIMAL(10,2) '1.5', DOUBLE PRECISION '1' keep their cast reading. This second layer is needed for the issue's own SELECT.
  4. The expression branch is wired through deparser (DmlDeParserSupport), TablesNamesFinder, SelectValidator, StatementFeatureVisitor, and the generic SelectVisitorAdapter traversal (subquery aliases stay on the select path, other expressions go to the expression visitor). SEARCH/CYCLE suffixes accepted by the tolerant grammar serialize on the expression branch too.

Root cause

Two layers. First, WithItem() only had the CTE and WITH FUNCTION forms; the whole WITH <expr> AS <ident> family was missing (verified against docker ClickHouse server 26.8.2.7: 16 shapes legal, including the undocumented bare x -> x*2 and chained total * 2 AS doubled). Second, isImplicitCastAhead() classified every DATA_TYPE head as a typed-literal prefix, sending SELECT double(5), SELECT number FROM t, and sum(number) into a branch that requires a following literal.

Testing

  • Red to green on master dec8f5d: the issue query and SELECT double(5) fail there and pass with this change; classic CTE and typed-literal guards remain unchanged. Further guards cover nested WITH, INSERT, mixed expression aliases and CTEs, MATERIALIZED and invalid input.
  • The review follow-up adds 13 regression cases for bracket groups at 4095, 4096 and 4097 tokens and beyond, large CTE column lists, nested calls, qualified function names, incomplete input and cancellation. Over-limit expressions and cancellation checks fail on the reviewed revision 06f1556 and pass with the follow-up. A mutation that always chooses the expression branch is rejected by the CTE guard.
  • JDK 17: full mvn clean verify (7312 tests, 25 skipped) and gradlew check (7330 tests, 25 skipped) passed with the scanner fix. After incorporating the separate visitor-assertion follow-up, all 64 ClickHouse tests passed again with both build tools.
  • Project JMH JSQLParserBenchmark.parseSQLStatements, standard 54-statement corpus, version=latest, one thread, 3 forks, 2 x 5-second warmups and 5 x 1-second measurements per fork: master/fixed/master measured 78.1 ± 25.6 / 57.1 ± 3.4 / 55.6 ± 2.2 ms/op (99.9% confidence intervals, 15 samples each). The intervals overlap; this benchmark shows no clear regression. Both versions produce the same serialized statements.
  • The same benchmark and settings on a single 2047-argument WITH function (4095 tokens in its bracket group, accepted by both versions) measured 111.3 ± 25.6 ms/op at 06f1556 and 12.3 ± 2.8 ms/op with the fix. The parsed output matches. This improvement is specific to that long-expression input.

Behavior notes

  • Tolerated shapes that now parse where master errored, each deparsing and re-parsing stably: WITH (a, b) AS c, WITH t(a, b) AS u, WITH ? AS p, WITH TRUE AS flag, WITH int(5) AS f, WITH number AS n, WITH (x, y) -> x + y AS m.
  • In an alias position, PG's data -> 'x' reads as a lambda; master rejected both readings there. ->>/#> are unaffected.
  • Not implemented: ClickHouse's trailing comma after the last WITH item. Single-parameter lambdas deparse without their parameter parentheses; the existing LambdaExpression.toString() convention, shared with function-argument lambdas.

Verification of the original issue

On master dec8f5d, WITH (x -> x * 2) AS double SELECT double(5) fails to parse. With this change the WITH item carries the lambda (ParenthesedExpressionList wrapping a LambdaExpression) with alias double, the select item parses as the function call double(5), and the statement round-trips. All ClickHouse syntax evidence comes from docker clickhouse-server 26.8.2.7; no claim is made about running the reporter's JSqlParser version.

Fixes #2632

…mbdas

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 JSQLParser#2632

Signed-off-by: 付典 <fudianchn@gmail.com>
private int skipBalancedBracketGroup(int i) {
int depth = 0;
int guard = 0;
while (guard++ < 4096) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this 4096 boundary come from? Looks rather arbitrary to me. What if there are more characters inside that bracket?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 4096 limit was arbitrary and could reject a valid WITH expression. 5fd7241 removes it and walks the token chain until the matching closing bracket or EOF, stopping on cancellation. Qualified names go directly to the expression branch, so the separate 64 limit is gone too.

Regression tests cover 4095/4096/4097 tokens and beyond, nested calls, large CTE column lists, incomplete input and cancellation.

The ClickHouse visitor-adapter test declared a list that was never
filled or asserted, and its WithItem override only called super, so the
claimed expression-visitor hand-off was not actually verified (Codacy:
unused local variable). Pass a collecting ExpressionVisitorAdapter to
the SelectVisitorAdapter and assert the lambda text it receives.

Signed-off-by: 付典 <fudianchn@gmail.com>
Signed-off-by: 付典 <fudianchn@gmail.com>
@fudianchn
fudianchn marked this pull request as draft September 17, 2026 03:40
@fudianchn
fudianchn marked this pull request as ready for review September 17, 2026 03:42
@manticore-projects
manticore-projects merged commit 0036c75 into JSQLParser:master Sep 17, 2026
10 checks passed
@manticore-projects

Copy link
Copy Markdown
Contributor

Thank you! Although we are reaching the point where even I am not certain anymore, what JSQLParser supports (or not). :-D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] JSQLParser : ClickHouse : Lambda expression in WITH clause is not supported

2 participants