Summary
After upstream PR ClickHouse/ClickHouse#94748 (Fix invalid result of joining two -Cluster table functions, commit 5b364db3ae0, merge 2b769e0ae56, present on antalya-26.3 but not on antalya-26.1/25.8), the analyzer unconditionally wraps the leftmost IStorageCluster table in a subquery whenever a JOIN is present. As a result, object_storage_cluster_join_mode='local' and 'allow' are now behaviorally indistinguishable.
The local-mode code path in IStorageCluster is dead code for cluster-leftmost JOINs.
Why local is now redundant
src/Planner/PlannerJoinTree.cpp:2828–2865 rewrites the query tree from
SELECT … FROM cluster_fn(...) AS t1 JOIN local AS t2 ON …
into
SELECT … FROM (SELECT <cols> FROM cluster_fn(...)) AS t1 JOIN local AS t2 ON …
before IStorageCluster::read is called. IStorageCluster then receives only the inner column-fetch subquery — no JOIN, no CROSS JOIN, no local-column WHERE. So:
IStorageCluster::updateQueryWithJoinToSendIfNeeded (src/Storages/IStorageCluster.cpp:212–303): getQueryTreeInfo reports no join ⇒ the LOCAL branch is a no-op, identical to the ALLOW branch.
IStorageCluster::getQueryProcessingStage (src/Storages/IStorageCluster.cpp:628–651): same — both modes fall through to the same WithMergeableState path.
The JOIN executes on the initiator regardless of mode; shards only see SELECT cols FROM cluster_fn(...).
Summary
After upstream PR ClickHouse/ClickHouse#94748 (Fix invalid result of joining two
-Clustertable functions, commit5b364db3ae0, merge2b769e0ae56, present onantalya-26.3but not onantalya-26.1/25.8), the analyzer unconditionally wraps the leftmostIStorageClustertable in a subquery whenever a JOIN is present. As a result,object_storage_cluster_join_mode='local'and'allow'are now behaviorally indistinguishable.The
local-mode code path inIStorageClusteris dead code for cluster-leftmost JOINs.Why
localis now redundantsrc/Planner/PlannerJoinTree.cpp:2828–2865rewrites the query tree frominto
before
IStorageCluster::readis called.IStorageClusterthen receives only the inner column-fetch subquery — no JOIN, no CROSS JOIN, no local-columnWHERE. So:IStorageCluster::updateQueryWithJoinToSendIfNeeded(src/Storages/IStorageCluster.cpp:212–303):getQueryTreeInforeports no join ⇒ theLOCALbranch is a no-op, identical to theALLOWbranch.IStorageCluster::getQueryProcessingStage(src/Storages/IStorageCluster.cpp:628–651): same — both modes fall through to the sameWithMergeableStatepath.The JOIN executes on the initiator regardless of mode; shards only see
SELECT cols FROM cluster_fn(...).