Fix Inconsistent AST formatting logical error on CREATE INDEX with extra-parenthesized expression#109175
Conversation
…] INDEX with extra-parenthesized expression CREATE HYPOTHETICAL INDEX i0 ON t0 ((a())) TYPE a (and the plain CREATE INDEX form) aborted the server with an "Inconsistent AST formatting" logical error in debug and sanitizer builds (issue ClickHouse#109163). ParserCreateIndexDeclaration consumes the index's own opening bracket, then parses the inner content as an ORDER BY list. When the single index expression is itself wrapped in extra parentheses (e.g. ((a())) -> inner (a())), RoundBracketsLayer sets the parenthesized flag on the resulting expression node. On formatting, that flag re-emits the parens as (a()); on re-parsing, the parser consumes those parens as the index bracket, so the re-parsed expression has no parenthesized flag and formats as a(). The format-parse-format round trip therefore diverges by one paren level and trips the consistency check in executeQueryImpl. The index bracket already groups the expression, so a top-level parenthesized flag is redundant grouping that cannot round-trip. Clear it after extracting the single expression. Multi-element tuples and alias expressions are unaffected: the tuple parens are part of the value representation, and alias parens are emitted via the formatter's need_parens path, not the parenthesized flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
cc @evillique @george-larionov for review, and @PedroTadim (this unblocks the fuzzer features PR). The fix is in the shared |
|
Workflow [PR], commit [3c14b20] Summary: ❌
AI ReviewSummaryThis PR fixes several Final Verdict
|
…al form starts with a parenthesis The previous blanket setParenthesized(false) in ParserCreateIndexDeclaration fixed the redundant-parenthesization class (e.g. ((a()))), but a second, independent class still broke the format-parse-format round trip: a single index expression whose canonical form itself starts with a `(` that closes before the end of the expression. Examples: (a, b).1 (tupleElement of a leading tuple), (x, y) -> x (lambda with tuple params), (a + b) * c (leading precedence group). ParserCreateIndexDeclaration always consumes one leading `(` as the index's own bracket, so formatting these bare lets the re-parse swallow that `(` as the bracket, split the inner list, and drop the trailing operator, producing a SYNTAX_ERROR or an AST that diverges on re-format. This is not a parenthesized-flag issue, so clearing the flag cannot fix it. Some of these forms were already broken on master before the flag change; this addresses the whole class. Fix in ASTIndexDeclaration::formatImpl: for a single expression in a CREATE INDEX declaration, format it, and if the result starts with a `(` whose matching `)` is not the last character, re-wrap it in an extra `(...)` so the index bracket stays distinguishable. Forms whose leading `(` already encloses the whole expression (a bare tuple (a, b), (expr AS alias)) are left unchanged, so multi-column index formatting is unaffected. Parentheses inside string / quoted-identifier literals are ignored by the check. Extended the regression test with round-trip and concrete-output cases for the leading-parenthesis class and non-regression cases for multi-element tuples and aliases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Follow-up (addressing the bot review): the fix now also covers the leading-parenthesis class via Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-10:20260702-133100 |
…EX round-trip leadingParenClosesEarly only re-wrapped single expressions whose first `(` closes before the end of the formatted text. A scalar subquery / VALUES expression formats as `(SELECT ...)` with its matching `)` at the very end, so the scan missed it: `CREATE INDEX i ON t ((SELECT 1)) TYPE minmax` formatted to `(SELECT 1)`, and re-parsing consumed that leading `(` as the index bracket, leaving a bare `SELECT 1` that ParserOrderByExpressionList rejects. Key the re-wrap off the AST kind for the ASTSubquery class in addition to the early-closing scan. Skip it when the expression already carries an alias (the alias forces its own enclosing bracket via need_parens), so aliased subqueries are not over-wrapped. Extends the 04502 regression test with `((SELECT 1))`, `((VALUES (1)))`, a correlated subquery, HYPOTHETICAL, and the aliased-subquery non-regression case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ASTIndexDeclaration::clone rebuilt the declaration but copied only granularity, dropping part_of_create_index_query. That flag selects the CREATE INDEX formatting (index bracket plus the extra wrapper this PR restores for parenthesized expressions), so a clone that loses it formats via the column-list branch and no longer round-trips: clone()->format() diverged from format(). ASTHypotheticalIndexQuery::clone already worked around this by re-setting the flag on the cloned declaration; fixing clone() itself covers that path and any other clone site, so the workaround is removed. Adds a gtest asserting clone()->format() == format() and a format-reparse round trip for the parenthesized CREATE INDEX / CREATE HYPOTHETICAL INDEX / ON CLUSTER forms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pre-PR validation gate for the Pre-PR validation gate (click to expand)
Note: I could not reproduce a user-visible Session id: cron:clickhouse-worker-slot-3:20260702-163000 |
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 50/59 (84.75%) · Uncovered code |
CI finish ledger — 3c14b20CI is complete on this head. No failure below is caused by this PR's diff (a Parsers AST-formatting round-trip fix + regression tests). Only the private
Session id: cron:our-pr-ci-monitor:20260703-000000 |
CI finish ledger — 3c14b20Every failure below has an owner. Only
No PR-caused failure on this SHA. #109175's diff is a one-line Session id: cron:our-pr-ci-monitor:20260703-013000 |

Closes: #109163
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed an
Inconsistent AST formattinglogical error (server abort in debug and sanitizer builds) forCREATE INDEX/CREATE HYPOTHETICAL INDEXexpressions that could not survive a format-parse-format round trip, for exampleCREATE HYPOTHETICAL INDEX i0 ON t0 ((a())) TYPE aandCREATE INDEX i0 ON t0 ((a, b).1) TYPE minmax.Description
Reported in #109163.
CREATE HYPOTHETICAL INDEX i0 ON t0 ((a())) TYPE a(and the plainCREATE INDEXform) raised anInconsistent AST formattinglogical error, which aborts the server in debug and sanitizer builds. There are two independent causes, both fixed here.Redundant parenthesization flag.
ParserCreateIndexDeclarationconsumes the index's own opening bracket and parses the inner content as an ORDER BY list. When the single index expression is itself wrapped in extra parentheses (((a()))-> inner(a())),RoundBracketsLayersets theparenthesizedflag on the resulting node. Formatting re-emits it as(a()); on re-parsing, the parser consumes those parens as the index bracket, so the re-parsed node has no flag and formats asa(). The round trip diverges by one paren level. Fix: clear the redundant top-levelparenthesizedflag after extracting the single expression (the index bracket already groups it).Canonical form starting with a parenthesis. A single expression whose formatted form itself starts with a
(that closes before the end ((a, b).1,(x, y) -> x,(a + b) * c) has noparenthesizedflag, but formatting it bare after the index keyword lets the re-parse swallow that leading(as the index bracket, split the inner list, and drop the trailing operator (a SYNTAX_ERROR or a diverging AST). Some of these were already broken before this PR. Fix: inASTIndexDeclaration::formatImpl, re-wrap such single expressions in an extra(...)so the index bracket stays distinguishable. Forms whose leading(already encloses the whole expression (a bare tuple(a, b),(expr AS alias)) are left unchanged, so multi-column index formatting is unaffected. Parentheses inside string / quoted-identifier literals are ignored by the check.Regression test:
tests/queries/0_stateless/04502_inconsistent_ast_create_index_expr_paren_round_trip.sql.