Fix MULTIPLE_EXPRESSIONS_FOR_ALIAS with enabled parallel replicas#103806
Conversation
…eplicas
When parallel replicas are enabled with `parallel_replicas_local_plan = 0`,
the analyzed query tree is serialized back to AST and sent to remote replicas,
which re-analyze it. The analyzer's expansion of an alias reference (e.g.
`PREWHERE cond` where `cond` is a projection alias) preserves the aliases on
the expanded body, while the matching projection column may not carry the same
inner aliases. The remote replica's analyzer then sees the same alias bound to
two different bodies and throws `MULTIPLE_EXPRESSIONS_FOR_ALIAS`.
This patch postprocesses the AST produced by `queryNodeToDistributedSelectQuery`
to:
1. Strip aliases from constraint clauses (`PREWHERE` / `WHERE` / `HAVING` /
`QUALIFY`), recursively, stopping at subquery boundaries (subqueries have
their own alias scope). The alias on the projection column is preserved
and is what the receiver actually needs.
2. Drop duplicate aliases in the projection list. `SELECT *` over joined
subqueries with overlapping column names produces multiple projection
columns sharing an alias (e.g. `__table1.date AS date, __table3.date AS date`),
which the remote replica's analyzer rejects. Position-aligned data is
all that matters across replicas; the user-visible column names come from
the coordinator's projection metadata, not the AST.
Tests in `tests/parallel_replicas_blacklist.txt` blacklisted under issue ClickHouse#74324
(`00597_push_down_predicate_long`, `02559_multiple_read_steps_in_prewhere`)
now pass with parallel replicas enabled, so they are removed from the
blacklist. A dedicated regression test is added in
`04145_multiple_expressions_for_alias_parallel_replicas.sql`.
Closes ClickHouse#74324.
Session: cron:clickhouse-ci-task-worker:20260430-191500
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
cc @KochetovNicolai @nickitat @alexey-milovidov — could you review this? It postprocesses the AST emitted by |
|
Workflow [PR], commit [4fd0267] Summary: ✅
AI ReviewSummaryThis PR fixes Final VerdictStatus: ✅ Approve |
The previous commit added two changes to `queryNodeToDistributedSelectQuery`: 1. Strip aliases from constraint clauses (`PREWHERE`/`WHERE`/`HAVING`/`QUALIFY`) — this is the actual fix for issue ClickHouse#74324. 2. Drop duplicate aliases in the projection list — this broke distributed column lookup and caused `THERE_IS_NO_COLUMN` errors in three Fast tests: - `03825_exists_constant_folded_action_name` - `01018_Distributed__shard_num` - `01890_materialized_distributed_join` Symptom (`01890_materialized_distributed_join`): Cannot find column `v` in source stream, there are only columns: [v, k, __table1.v, tl.v, tl.k, __table1.v] The remote replica resolves columns of the source stream by their alias name. The projection list `td.v, td.k, td.v, tl.v, tl.k, td.v` analyses to three identical `__table1.v AS v` projections plus three other columns. Stripping the alias on the 2nd and 3rd `v` columns left them named `__table1.v` instead of `v`, so the receiver could no longer match position-by-name and threw `THERE_IS_NO_COLUMN`. The conflict the dedup was trying to solve (genuine duplicate-alias-different-body, e.g. `__table1.name AS name, __table2.name AS name` in a self-join) is the analyzer's business — it already disambiguates user-visible aliases via `tl.a`-style table-qualified projections or via `joined_subquery_requires_alias`. Stripping projection aliases on the distributed-AST path just breaks position-by-name resolution at the receiver. Drop the dedup. The constraint-clause alias stripping (which is the real fix for ClickHouse#74324) is unchanged. CI: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=103806&sha=7d024a7acb7b496ca17c259e9dc4d3e66d45c0a4&name_0=PR&name_1=Fast%20test PR: ClickHouse#103806 Issue: ClickHouse#74324 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Pushed a fix for the 3 Fast test failures. Root causeThe original commit added two changes:
The dedup stripped the alias from any projection column whose alias name was already seen, regardless of whether the body actually differed. For example, in SELECT td.v, td.k, td.v, tl.v, tl.k, td.v FROM test_distributed td asof JOIN test_local tl ...The three Same shape for FixRemove the projection dedup. The constraint-clause alias stripping (the actual fix for #74324) is unchanged. Genuine duplicate-alias-different-body conflicts in Pre-PR validation gate
Session: cron:clickhouse-ci-task-worker:20260501-101500 |
Commit 7d024a7 added two changes to `queryNodeToDistributedSelectQuery`: 1. Strip aliases from constraint clauses (`PREWHERE` / `WHERE` / `HAVING` / `QUALIFY`) — the actual fix for ClickHouse#74324. 2. Drop ALL duplicate aliases in the projection list — broke distributed column lookup in three Fast tests (`03825_exists_constant_folded_action_name`, `01018_Distributed__shard_num`, `01890_materialized_distributed_join`) because it stripped same-body duplicates that the receiver still expects to be reachable by the original alias. Commit 677e81b removed the projection dedup entirely, which fixed those three Fast tests but reintroduced the original bug for `SELECT *` over a self-join with `joined_subquery_requires_alias = 0` — case 2 of the regression test `04145_multiple_expressions_for_alias_parallel_replicas`. The CI failure (12 stateless variants) showed: Code: 179. DB::Exception: Multiple expressions __table3.name AS name and __table1.name AS name for alias name. Two distinct projection bodies (`__table1.name` and `__table3.name`) shared the alias `name`, which the remote replica's analyzer rejects. This patch puts dedup back, but conditioned on the body of the projection expression. For each alias seen in the projection list, record the `getTreeHash(ignore_aliases=true)` of the FIRST occurrence; for any later occurrence with the same alias, strip the alias only when the body hash differs. - `__table1.name AS name`, `__table3.name AS name` (case 2 of ClickHouse#74324): different bodies → strip the alias on `__table3.name`. The receiver sees [id, name, value, __table3.name, __table3.value] — no alias conflict, no `MULTIPLE_EXPRESSIONS_FOR_ALIAS`. - 3x `__table1.v AS v` from `SELECT td.v, td.k, td.v, tl.v, tl.k, td.v` in `01890_materialized_distributed_join`: same body, same alias → keep the alias on every occurrence. The receiver's source stream keeps a column named `v` at every position the original projection expected, so position-by-name lookup still works. The constraint-clause alias stripping (the actual fix for ClickHouse#74324) is unchanged. Verified locally on a debug build: - `04145_multiple_expressions_for_alias_parallel_replicas` (all 3 cases) - `03825_exists_constant_folded_action_name` - `01018_Distributed__shard_num` - `01890_materialized_distributed_join` - `02559_multiple_read_steps_in_prewhere` - `00597_push_down_predicate_long` CI: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=103806&sha=677e81b75bfdc8621e908132a56d0abed7be9f6d&name_0=PR PR: ClickHouse#103806 Issue: ClickHouse#74324 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iteration: re-add projection alias dedup, body-hash-awareCI on commit That's case 2 of the regression test ( Pushed: 32f3f01The new commit re-adds the projection dedup but conditions it on the body of the expression. For each alias, the first occurrence wins; later occurrences are stripped only when their body differs. std::unordered_map<String, IASTHash> alias_to_first_body_hash;
for (auto & child : projection_ast->children)
{
auto * with_alias = dynamic_cast<ASTWithAlias *>(child.get());
if (!with_alias) continue;
const auto & alias = with_alias->alias;
if (alias.empty()) continue;
const auto body_hash = child->getTreeHash(/*ignore_aliases=*/true);
auto [it, inserted] = alias_to_first_body_hash.emplace(alias, body_hash);
if (!inserted && it->second != body_hash)
with_alias->setAlias(String());
}This handles both classes of duplication consistently:
The constraint-clause alias stripping (the actual fix for #74324) is unchanged. Pre-PR validation gatea) Deterministic repro? Yes — b) Root cause explained? Yes — when c) Fix matches root cause? Yes — the fix targets exactly the difference: dedup only when bodies differ, leave same-body duplicates alone. d) Test intent preserved? Yes — e) Both directions demonstrated? Yes — locally verified:
f) Fix is general? Yes — the dedup operates on every projection alias in every distributed query AST, using a body hash that ignores aliases at all levels. It correctly handles both observed manifestations of the conflict and is a single mechanism rather than a per-pattern patch. Awaiting CI rerun. Session: cron:clickhouse-ci-task-worker:20260501-144500 |
| /// boundaries to keep the inner aliases intact. | ||
| /// | ||
| /// See https://github.com/ClickHouse/ClickHouse/issues/74324. | ||
| void stripAliasesFromConstraintExpression(ASTPtr ast) |
There was a problem hiding this comment.
A question - isn't ASTVisitor meant for that? Is it anyhow better or more practical to use here (this function is simple, so I'm not sure)?
Also, why it is named "FromConstraintExpression"? - the implementation does not depend on that. So the name can be more generic.
| /// alias) preserves the alias on the expanded body, while the projection has the same | ||
| /// body without inner aliases. The remote replica's analyzer then sees the same alias | ||
| /// (e.g. `cond`) attached to two different bodies and throws | ||
| /// `MULTIPLE_EXPRESSIONS_FOR_ALIAS`. Stripping aliases from constraint clause |
There was a problem hiding this comment.
Also a question - why the confusion with aliases arises in "constraints expressions" specifically? Can't it appear in other clauses?
| stripAliasesFromConstraintExpression(select_query->having()); | ||
| stripAliasesFromConstraintExpression(select_query->qualify()); | ||
|
|
||
| /// 2. Strip duplicate projection aliases ONLY when the duplicate has a |
There was a problem hiding this comment.
Sounds like a mechanical fixup of something that is already broken. Can't we fix it on a deeper level?
There was a problem hiding this comment.
@groeneai, duplicate projection aliases should not happen. Why do you need it?
There was a problem hiding this comment.
Good question — they shouldn't, but in one specific configuration they do, and that's case 2 of the regression test. Trace:
SET joined_subquery_requires_alias = 0;
SELECT * FROM (SELECT * FROM t) ANY LEFT JOIN (SELECT * FROM t) USING id;
-- t has columns: id, name, value- The matcher (unqualified
*) resolves to ColumnNodes forid, name, valuefrom the left subquery andname, valuefrom the right subquery (USING dedupsid). QueryAnalyzer::resolveMatcher(Resolve/QueryAnalyzer.cpp:2207) emitsProjectionNamescontaining each ColumnNode'scolumn_name. For self-joins the original column names collide: the names list is[id, name, value, name, value]. ColumnNode pointers are distinct (different table sources) so the query tree is unambiguous; only the names collide.QueryAnalyzer::resolveProjectionExpressionNodeList(Resolve/QueryAnalyzer.cpp:3568) buildsNamesAndTypes projection_columnsdirectly from those names, preserving the duplicates.QueryNode::toASTImpl(Analyzer/QueryNode.cpp:514-516) then sets each AST projection's alias fromprojection_columns[i].nameunconditionally, producing__table1.name AS name, __table3.name AS namein the AST sent to remote replicas. The receiver's analyzer rejects the same alias bound to two different bodies →MULTIPLE_EXPRESSIONS_FOR_ALIAS.
This only surfaces with parallel_replicas_local_plan = 0 AND joined_subquery_requires_alias = 0 AND SELECT * over a self-join. With joined_subquery_requires_alias = 1 (the default) the user is forced to alias the subqueries ((SELECT * FROM t) AS A), which qualifies columns and avoids the collision. So locally the query tree is fine; only the AST round-trip is broken.
The deeper-level fix would go in QueryNode::toASTImpl (or in resolveProjectionExpressionNodeList): when emitting the AST projection list, skip setting the alias on a later projection whose name collides with an earlier one bound to a different body. That's the same body-hash check we have here in Utils.cpp, just moved one layer up. I'd combine it with the PREWHERE analyzer fix from the other thread and remove this whole Utils.cpp postprocess.
Does that direction work for you? If yes I'll fold it into the same commit set as the QueryAnalyzer.cpp PREWHERE fix.
|
Thanks for the careful read — the three comments hit three real things, and the third one is the most important. Let me address each, then propose a deeper fix. (1) "Why
|
| Clause | Master result |
|---|---|
PREWHERE cond |
fails with MULTIPLE_EXPRESSIONS_FOR_ALIAS |
WHERE cond |
passes |
HAVING cond |
passes |
ORDER BY cond |
passes |
GROUP BY cond |
passes |
So in practice only PREWHERE triggers the asymmetry. The other clauses I included in this PR (WHERE / HAVING / QUALIFY) are dead code — happy to limit it to PREWHERE only. The reason PREWHERE is special is the third comment's territory.
(3) "Mechanical fixup of something that is already broken. Can't we fix it on a deeper level?"
Agreed — the analyzer is producing an asymmetric AST and the postprocess is hiding it. The deeper bug is in src/Analyzer/Resolve/QueryAnalyzer.cpp. Trace for the issue's repro:
SELECT cast(id1 AS UInt16) AS cond1, (id2 % 40000) AS cond2, (cond1 AND cond2) AS cond
FROM t PREWHERE condThe receiver sees:
SELECT
CAST(__table1.id1, 'UInt16') AS cond1,
__table1.id2 % 40000 AS cond2,
CAST(__table1.id1, 'UInt16') AND (__table1.id2 % 40000) AS cond -- no inner aliases
FROM default.t AS __table1
PREWHERE
(CAST(__table1.id1, 'UInt16') AS cond1) AND ((__table1.id2 % 40000) AS cond2) AS cond -- inner aliases preservedThe bodies differ: the projection's cond body has inner cond1/cond2 aliases stripped, while PREWHERE's retains them.
Why PREWHERE ends up different
-
Projection resolution (line 5479). When
cond1/cond2identifiers insidecond1 AND cond2are resolved,tryResolveIdentifierFromAliases(line 1062) deep-clones each alias node and registers the clone inscope.aliases.node_to_remove_aliases:alias_node = alias_node->clone(); scope_to_resolve_alias_expression->aliases.node_to_remove_aliases.push_back(alias_node);The projection's
condbody now contains tracked clonesC1/C2ofnode1/node2— they will get their aliases cleared at line 5683. -
PREWHERE resolution (line 5486). The
condidentifier resolves to a deep cloneDof the AND node.Dis registered innode_to_remove_aliases, but its children — fresh deep copiesD_c1/D_c2ofnode1/node2— carry the originalcond1/cond2aliases (clone preserves aliases) and are not registered. -
Line 5501 — second deep clone.
prewhere_node = prewhere_node->clone(); // E = D.clone() ReplaceColumnsVisitor replace_visitor(scope.join_columns_with_changed_types, scope.context); replace_visitor.visit(prewhere_node);
prewhere_nodeis replaced by a second deep clone so thatReplaceColumnsVisitor(forJOIN USINGtype changes) can rewrite the prewhere copy without leaking back into the projection. The new treeE(with childrenE_c1/E_c2) is yet another set of alias-bearing copies, none registered. The originalD— the one that was innode_to_remove_aliases— is no longer in the query tree. -
Alias removal at line 5683 is non-recursive.
for (auto & node : scope.aliases.node_to_remove_aliases) node->removeAlias();
Only registered roots get cleared. The projection's tree is fine because every
cond1/cond2clone in it (C1/C2) was registered.PREWHERE's tree's inner clones —E_c1/E_c2— aren't reached.
That's why the projection's body is alias-free and PREWHERE keeps inner aliases. It's PREWHERE-specific because PREWHERE is the only clause that gets the second deep clone (no other clause runs ReplaceColumnsVisitor).
Proposed deeper fix
Two orthogonal changes:
A. Make alias removal recursive. Replace the loops at lines 5680-5689 with a top-down InDepthQueryTreeVisitor walk that calls removeAlias() on every descendant of each registered node, stopping at QUERY / UNION / LAMBDA boundaries (those have their own scopes). This catches aliases on nodes that aren't directly registered but live inside a registered subtree.
B. Register the post-clone prewhere_node. After line 5503, push the new clone into node_to_remove_aliases:
prewhere_node = prewhere_node->clone();
ReplaceColumnsVisitor replace_visitor(scope.join_columns_with_changed_types, scope.context);
replace_visitor.visit(prewhere_node);
scope.aliases.node_to_remove_aliases.push_back(prewhere_node); // NEWCombined with A, the recursive walk over prewhere_node clears E, E_c1, E_c2. The asymmetry disappears.
With both in place, queryNodeToDistributedSelectQuery no longer needs to touch PREWHERE at all.
This is QueryAnalyzer-core territory — there are passes that read aliases off intermediate nodes (e.g. nodes_with_duplicated_aliases handling at lines 5588-5657, optimize_functions_to_subcolumns rewrites). I'd want to run the full functional + integration suite, including the analyzer-touch tests, before claiming the deeper fix is safe.
What about the regression test's case 2?
04145 case 2 is SELECT * over a self-join with joined_subquery_requires_alias = 0:
SELECT * FROM (SELECT * FROM t) ANY LEFT JOIN (SELECT * FROM t) USING idThis produces a projection list with two columns sharing alias name but bound to different sources (__table1.name vs __table3.name). Different root cause — it's about *-expansion producing duplicate projection aliases, not PREWHERE reuse. The body-hash-aware dedup in commit 32f3f01 handles this, but it's also mechanical. The deeper fix here would live in matcher resolution / *-expansion (have the analyzer either disambiguate the alias or drop it before the AST round-trip), and is independent of the PREWHERE fix above.
Plan
- Small (any time): rename the helper, drop the dead
WHERE/HAVING/QUALIFYpaths. - Deeper fix for MULTIPLE_EXPRESSIONS_FOR_ALIAS with enabled Parallel Replicas #74324 (this PR or separate): implement A+B in
QueryAnalyzer.cpp, drop thePREWHEREpostprocessing, run the full test suite. Happy to do this here if you'd like it in one PR — just want a heads-up that an analyzer change is in scope. - Case 2 (
SELECT *duplicate aliases): I'd lean toward a separate PR focused on matcher /*-expansion disambiguation.
Let me know which direction you want to go and I'll push.
| stripAliasesFromConstraintExpression(select_query->prewhere()); | ||
| stripAliasesFromConstraintExpression(select_query->where()); | ||
| stripAliasesFromConstraintExpression(select_query->having()); | ||
| stripAliasesFromConstraintExpression(select_query->qualify()); |
There was a problem hiding this comment.
@groeneai This logic could be done on top of the query tree.
There was a problem hiding this comment.
Yes — fully agree. I posted a deeper-level proposal in #103806 (comment) (reply to @alexey-milovidov on the same mechanical fixup thread): the analyzer's node_to_remove_aliases walk in QueryAnalyzer::resolveQuery is non-recursive, and the post-clone prewhere_node (at Resolve/QueryAnalyzer.cpp:5501) is never registered for cleanup, which is why inner aliases survive on PREWHERE's body but not on the projection's. Fix is two changes inside QueryAnalyzer.cpp (recursive removeAlias walk + register post-clone prewhere_node), then this whole Utils.cpp postprocess for PREWHERE/WHERE/HAVING/QUALIFY goes away.
I was waiting on @alexey-milovidov to confirm the direction before scoping the analyzer change into this PR (vs a separate PR). With your independent agreement, I'd like to proceed with the deeper fix in this PR — let me know if you want to weigh in before I push it. The case-2 self-join projection-alias issue (your other comment) is a separate root cause and I'd address it at the deeper level too — see my reply on that thread.
| stripAliasesFromConstraintExpression(select_query->having()); | ||
| stripAliasesFromConstraintExpression(select_query->qualify()); | ||
|
|
||
| /// 2. Strip duplicate projection aliases ONLY when the duplicate has a |
There was a problem hiding this comment.
@groeneai, duplicate projection aliases should not happen. Why do you need it?
| @@ -0,0 +1,74 @@ | |||
| -- Tags: long, no-parallel | |||
There was a problem hiding this comment.
@groeneai Why no-parallel? Maybe it could be dropped?
There was a problem hiding this comment.
Good catch — dropped in 7d4c68a. The test creates uniquely-named tables (t_74324_*) that are dropped at the end and uses session-scoped settings, so it has no shared state that requires sequential execution. The long tag is kept because the test exercises three configurations against parallel replicas.
The test creates uniquely-named tables (`t_74324_*`) that are dropped at the end and uses session-scoped settings, so it has no shared state that requires sequential execution. Per @novikd's review on PR ClickHouse#103806.
|
Addressing the |
…ysis` Per review comment (#80310 (comment)...), the query `SELECT *, day + 365 AS day` in this test triggers an unrelated analyzer bug (`MULTIPLE_EXPRESSIONS_FOR_ALIAS`, issue #74324, fix PR #103806 still open) when executed under parallel replicas. Random auto-PR settings randomization can enable parallel replicas in any CI suite, so CI was failing on the unrelated bug instead of validating #48881. Add `SET enable_parallel_replicas = 0` and `SET automatic_parallel_replicas_mode = 0` at the top of the test, with a comment pointing to #74324, so the guard can be removed once that bug is fixed. This keeps the test deterministic without resorting to the `no-parallel-replicas` tag or the `parallel_replicas_blacklist.txt` entry that earlier reviewers asked to remove. CI report: #80310 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arallel-replicas Resolve conflict in tests/parallel_replicas_blacklist.txt: - Master removed the 74335 / Bad get / 00502_sum_map block; keep that removal. - This branch removes the 74324 / MULTIPLE_EXPRESSIONS_FOR_ALIAS block (00597_push_down_predicate_long, 02559_multiple_read_steps_in_prewhere) because the alias-stripping fix in src/Planner/Utils.cpp makes those tests pass under parallel replicas. src/Planner/Utils.cpp auto-merged cleanly: our stripAliasesFromConstraintExpression helper coexists with master's standalone-subquery wrapping inside buildFilterInfo.
|
Rebased onto current master (merge commit
Diff vs
Still awaiting direction from @alexey-milovidov / @novikd on whether to fold the deeper analyzer-level fix (recursive |
Address reviewer feedback by moving the `PREWHERE` half of the fix out of `queryNodeToDistributedSelectQuery` and into the analyzer itself. The original PR postprocessed the AST after `queryNodeToDistributedSelectQuery` to clear aliases from constraint clauses. Reviewers (ClickHouse#103806 review threads at `Utils.cpp:214`, `:223`, `:267`) flagged the approach as a "mechanical fixup of something already broken" and asked for a deeper fix in the query tree. This commit implements that. Root cause (traced in `QueryAnalyzer::resolveQuery`): * When resolving `PREWHERE cond` where `cond` is a projection alias, the analyzer deep-clones the alias body and registers the clone's root in `scope.aliases.node_to_remove_aliases` (see line ~1135 of `QueryAnalyzer.cpp`). The clone's *descendants*, which carry the inner aliases (e.g. `cond1`, `cond2`) from the original projection body, are not registered. * `PREWHERE` then deep-clones the tree a second time so that `ReplaceColumnsVisitor` can rewrite types for `JOIN USING` without leaking back into the projection. After this clone the originally- registered node is no longer in the query tree, while the new clone's root carries the `cond` alias and is unregistered. * The cleanup loop at line ~5840 calls `removeAlias` on each registered node non-recursively, so the projection's inner aliases get cleared while `PREWHERE`'s do not. When the AST is shipped to a remote replica under `parallel_replicas_local_plan = 0` the receiver sees the same alias bound to two different bodies and throws `MULTIPLE_EXPRESSIONS_FOR_ALIAS`. Fix: 1. Make alias removal recursive in `QueryAnalyzer.cpp`: walk each registered node top-down and clear aliases on every descendant, stopping at `QUERY` / `UNION` / `LAMBDA` boundaries (those are separate scopes that handle their own cleanup). 2. Register the post-clone `prewhere_node` in `node_to_remove_aliases` so the recursive walk reaches its inner aliases. `Utils.cpp` no longer touches `PREWHERE` / `WHERE` / `HAVING` / `QUALIFY`. The empirical check in the issue thread showed only `PREWHERE` ever triggered the asymmetry; the rest of the constraint clauses were dead code in the postprocess and are dropped together with it. The duplicate-projection-alias dedup remains in `queryNodeToDistributedSelectQuery` for now: it covers case 2 of the regression test (`SELECT *` over a self-join with overlapping column names) and a different root cause is being addressed in ClickHouse#104303 by @devcrafter. Once that lands the dedup here can be removed. Verified locally with the new binary against the regression test `04145_multiple_expressions_for_alias_parallel_replicas` and the original repro from ClickHouse#74324; both pass. Existing alias tests (`00057_join_aliases`, `00138_table_aliases`, `00157_aliases_and_lambda_formal_parameters`) remain green. Refs ClickHouse#74324 Refs ClickHouse#103806 (comment) Refs ClickHouse#104303 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed What changed
Why this targets the right thingTrace from the comment thread, condensed: when
The two-line fix in the analyzer kills both halves: (A) the recursive walk reaches the inner clones; (B) registering the post-clone ValidationVerified locally against the new binary:
|
|
This was fixed by #105146. Let's update the branch. |
…expressions-for-alias-parallel-replicas
The clickhouse-gh review on PR ClickHouse#103806 (2026-05-23) flagged a missing test case for the alias body sequence `A, B, B` (e.g. `SELECT r.name, l.name, l.name` with `parallel_replicas_local_plan = 0`). Tracing the actual AST emitted by `queryNodeToDistributedSelectQuery` shows the analyzer disambiguates the second and third columns to a distinct alias `l.name`, so the alias groups in the sent AST are `name` (one occurrence) and `l.name` (two occurrences with the same body). The dedup then leaves both `l.name` entries intact via the body-hash equality branch and never collapses the body-B occurrences against body A. Added two cases to `04145_multiple_expressions_for_alias_parallel_replicas`: 4. `SELECT r.name, l.name, l.name` over a 2-way join (the example). 5. `SELECT *` over a 3-way self-join with `USING`, where the projection produces the same bare alias three times with bodies from three different sources, exercising the dedup beyond the existing 2-occurrence case.
…expressions-for-alias-parallel-replicas
LLVM Coverage ReportChanged lines: Changed C/C++ lines covered by tests: 33/36 (91.67%) | Lost baseline coverage: none · Uncovered code |
…y_analysis Issue #74324 (`MULTIPLE_EXPRESSIONS_FOR_ALIAS` under parallel replicas) was closed by the fix #103806, so the old comment ("remove this guard once #74324 is fixed") is misleading: it invites removing the guard now that #74324 is closed. However, #103806 covered other shapes of that bug, not the self-shadowing alias used by this test (`SELECT *, day + 365 AS day`, where `day` is also a column). Verified locally against master with #103806 present: under forced parallel replicas the remote replica's analyzer still sees two bodies for the alias `day` and throws `MULTIPLE_EXPRESSIONS_FOR_ALIAS`. The `SET enable_parallel_replicas = 0` guard is therefore still required for this test to stay green in the `ParallelReplicas` CI suite (the in-query `SET` overrides the suite-level forcing). Auto-PR statistics mode (`automatic_parallel_replicas_mode = 2`) already passes, since it does not actually execute with parallel replicas. Update the comment to state this accurately and tie removal of the guard to the analyzer handling this shape, not to #74324 being closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

When parallel replicas are enabled with
parallel_replicas_local_plan = 0,the analyzed query tree is serialized back to AST and sent to remote replicas,
which re-analyze it. The analyzer's expansion of an alias reference (e.g.
PREWHERE condwherecondis a projection alias) preserves the aliases onthe expanded body, while the matching projection column may not carry the same
inner aliases. The remote replica's analyzer then sees the same alias bound to
two different bodies and throws
MULTIPLE_EXPRESSIONS_FOR_ALIAS. The sameclass of failure also surfaced for
SELECT *over self-joins with overlappingcolumn names (multiple projection columns sharing the alias
date, etc.).This patch postprocesses the AST produced by
queryNodeToDistributedSelectQuery:PREWHERE/WHERE/HAVING/QUALIFY), recursively, stopping at subquery boundaries (eachsubquery has its own alias scope). The alias on the projection column is
preserved and is what the receiver actually needs.
SELECT *expandsjoined subqueries with overlapping column names, position-aligned data is
what matters across replicas; the user-visible column names come from the
coordinator's projection metadata, not the AST sent to the receiver.
Tests in
tests/parallel_replicas_blacklist.txtblacklisted under #74324(
00597_push_down_predicate_long,02559_multiple_read_steps_in_prewhere)now pass with parallel replicas enabled and are removed from the blacklist.
A dedicated regression test is added covering the three failure patterns
reported in the issue (
PREWHEREalias reference,SELECT *over aself-join, and subcolumn rewriting under
optimize_functions_to_subcolumns).Reproducer (fails on master, passes with this PR):
Closes #74324.
Cc @alexey-milovidov who requested this fix in #80310 (comment).
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fix
MULTIPLE_EXPRESSIONS_FOR_ALIASerrors thrown by remote replicas when running queries that reference projection aliases insidePREWHERE/WHERE/HAVING/QUALIFY(e.g.SELECT x AS a, y AS b, (a AND b) AS c FROM t PREWHERE c) orSELECT *over self-joins with overlapping column names, with parallel replicas andparallel_replicas_local_plan = 0. Closes #74324.Documentation entry for user-facing changes
Note
Medium Risk
Changes analyzer/planner alias handling during query-tree-to-AST conversion for parallel replicas, which could affect projection naming/semantics on distributed execution paths; mitigated by targeted hashing logic and new regression coverage.
Overview
Fixes
MULTIPLE_EXPRESSIONS_FOR_ALIASwhenparallel_replicas_local_plan = 0by ensuring alias cleanup is consistent between projection and constraint clauses after query-tree cloning.The analyzer now recursively removes leaked inner aliases from expanded alias bodies (stopping at nested-scope boundaries) and re-registers cloned
PREWHEREexpressions for alias removal. The distributed AST conversion additionally strips later duplicate projection aliases only when the alias name repeats with a different expression body, avoiding remote-side alias conflicts while preserving same-body duplicates.Updates tests by un-blacklisting affected parallel replica cases and adding a dedicated regression test
04145_multiple_expressions_for_alias_parallel_replicas.Reviewed by Cursor Bugbot for commit d1a626f. Bugbot is set up for automated code reviews on this repo. Configure here.
Version info
26.6.1.273