Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
317db7c
Remove object_storage_cluster_join_mode=local
ianton-ru Aug 18, 2026
13ca570
Make `object_storage_cluster_join_mode=allow` work with initiator-loc…
ianton-ru Aug 19, 2026
4f5e50f
Reuse existing helpers to rewrite cluster JOIN queries in `IStorageCl…
ianton-ru Aug 19, 2026
3545cbd
Recognize rewritten `GLOBAL IN` tables when stripping cluster queries
ianton-ru Aug 19, 2026
f10d74d
Make `object_storage_cluster_join_mode=global` work without the analyzer
ianton-ru Aug 19, 2026
40e4fcd
Merge branch 'antalya-26.6' into feature/antalya-26.6/remove_object_s…
ianton-ru Aug 27, 2026
ca92f57
Clear `order_by_all` and `group_by_all` after stripping a cluster join
ianton-ru Sep 29, 2026
0f70f78
Merge branch 'antalya-26.6' into feature/antalya-26.6/remove_object_s…
ianton-ru Sep 29, 2026
2222452
Merge branch 'antalya-26.6' into feature/antalya-26.6/remove_object_s…
ianton-ru Oct 13, 2026
076a4f1
Keep old-analyzer `RIGHT` and `FULL` joins on the initiator.
ianton-ru Oct 21, 2026
58e7683
Send the stripped query when a remote initiator runs an initiator-loc…
ianton-ru Oct 21, 2026
7a3c2b7
Do not push left-table predicates under initiator-local `RIGHT` and `…
ianton-ru Oct 21, 2026
f6c387f
Clear `GROUP BY` modifiers when an initiator-local `IN` subquery stri…
ianton-ru Oct 21, 2026
b31e327
Treat a column-less subquery table as initiator-local under the analy…
ianton-ru Oct 21, 2026
d626fdd
Strip an ordinary join that follows `ARRAY JOIN` from the shard query.
ianton-ru Oct 21, 2026
8a45e64
Keep `PASTE` joins on the initiator.
ianton-ru Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Core/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2135,9 +2135,9 @@ ClickHouse applies this setting when the query contains the product of object st

Possible values:

- `local` — Replaces the database and table in the subquery with local ones for the destination server (shard), leaving the normal `IN`/`JOIN.`
- `global` — Replaces the `IN`/`JOIN` query with `GLOBAL IN`/`GLOBAL JOIN.` Right table executes first and is added to the secondary query as temporay table.
- `allow` — Default value. Allows the use of these types of subqueries.
- `local` — deprecated, legacy mode. The same as 'allow'.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Format the allow setting value as inline code

Replace the prose quotation 'allow' with the inline-code form `allow`. This is a literal ClickHouse setting value, and the repository requires such literals in documentation and comments to be wrapped in inline code.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

)", 0) \
\
DECLARE(UInt64, max_concurrent_queries_for_all_users, 0, R"(
Expand Down
104 changes: 5 additions & 99 deletions src/Storages/IStorageCluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,7 @@ Converts
localtable as t
ON s3.key == t.key

to (object_storage_cluster_join_mode='local')

SELECT s3.c1, s3.c2, s3.key
FROM
s3Cluster(...) AS s3

or (object_storage_cluster_join_mode='global')
to (object_storage_cluster_join_mode='global')

SELECT s3.c1, s3.c2, t.c3
FROM
Expand All @@ -241,89 +235,8 @@ void IStorageCluster::updateQueryWithJoinToSendIfNeeded(
auto object_storage_cluster_join_mode = context->getSettingsRef()[Setting::object_storage_cluster_join_mode];
switch (object_storage_cluster_join_mode)
{
case ObjectStorageClusterJoinMode::LOCAL:
{
if (!context->getSettingsRef()[Setting::allow_experimental_analyzer])
throw Exception(ErrorCodes::NOT_IMPLEMENTED,
"object_storage_cluster_join_mode!='allow' is not supported without allow_experimental_analyzer=true");

auto info = getQueryTreeInfo(query_info.query_tree, context);

if (info.has_join || info.has_cross_join || info.has_local_columns_in_where)
{
auto modified_query_tree = query_info.query_tree->clone();

SearcherVisitor left_table_expression_searcher({QueryTreeNodeType::TABLE, QueryTreeNodeType::TABLE_FUNCTION}, 1, context);
left_table_expression_searcher.visit(modified_query_tree);
auto table_function_node = left_table_expression_searcher.getNode();
if (!table_function_node)
throw Exception(ErrorCodes::LOGICAL_ERROR, "Can't find left table function node");

QueryTreeNodePtr query_tree_distributed;

auto & query_node = modified_query_tree->as<QueryNode &>();

if (info.has_join)
{
auto join_node = query_node.getJoinTree();
query_tree_distributed = join_node->as<JoinNode>()->getLeftTableExpression()->clone();
}
else if (info.has_cross_join)
{
SearcherVisitor join_searcher({QueryTreeNodeType::CROSS_JOIN}, 1, context);
join_searcher.visit(modified_query_tree);
auto cross_join_node = join_searcher.getNode();
if (!cross_join_node)
throw Exception(ErrorCodes::LOGICAL_ERROR, "Can't find CROSS JOIN node");
// CrossJoinNode contains vector of nodes. 0 is left expression, always exists.
query_tree_distributed = cross_join_node->as<CrossJoinNode>()->getTableExpressions()[0]->clone();
}

// Find add used columns from table function to make proper projection list
// Need to do before changing WHERE condition
CollectUsedColumnsForSourceVisitor collector(table_function_node, context);
collector.visit(modified_query_tree);
const auto & columns = collector.getColumns();

if (columns.empty())
{
auto column_nodes_to_select = std::make_shared<ListNode>();
column_nodes_to_select->getNodes().reserve(1);
column_nodes_to_select->getNodes().emplace_back(std::make_shared<ConstantNode>(1));
query_node.getProjectionNode() = column_nodes_to_select;
}
else
{
query_node.resolveProjectionColumns(columns);
auto column_nodes_to_select = std::make_shared<ListNode>();
column_nodes_to_select->getNodes().reserve(columns.size());
for (auto & column : columns)
column_nodes_to_select->getNodes().emplace_back(std::make_shared<ColumnNode>(column, table_function_node));
query_node.getProjectionNode() = column_nodes_to_select;
}

if (info.has_local_columns_in_where)
{
if (query_node.getPrewhere())
removeExpressionsThatDoNotDependOnTableIdentifiers(query_node.getPrewhere(), table_function_node, context);
if (query_node.getWhere())
removeExpressionsThatDoNotDependOnTableIdentifiers(query_node.getWhere(), table_function_node, context);
}

query_node.getOrderByNode() = std::make_shared<ListNode>();
query_node.getGroupByNode() = std::make_shared<ListNode>();

if (query_tree_distributed)
{
// Left only table function to send on cluster nodes
modified_query_tree = modified_query_tree->cloneAndReplace(query_node.getJoinTree(), query_tree_distributed);
}

query_to_send = queryNodeToDistributedSelectQuery(modified_query_tree);
}

case ObjectStorageClusterJoinMode::LOCAL: // Legacy mode, equal to 'allow'
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve initiator-local joins for legacy local mode

When local is selected for an object-storage cluster query that joins a table existing only on the initiator—the scenario in tests/integration/test_s3_cluster/test.py::test_joins—returning here leaves the join in query_to_send, which is dispatched to every shard; for example, s0_1_0 cannot resolve the test's join_table. The previous LOCAL path stripped the join and forced FetchColumns so it ran on the initiator, whereas the new ALLOW behavior requests WithMergeableState; therefore existing local queries and the integration test fail rather than retaining compatible behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point!

}
case ObjectStorageClusterJoinMode::GLOBAL:
{
auto info = getQueryTreeInfo(query_info.query_tree, context);
Expand Down Expand Up @@ -690,22 +603,15 @@ IStorageCluster::QueryTreeInfo IStorageCluster::getQueryTreeInfo(QueryTreeNodePt
}

QueryProcessingStage::Enum IStorageCluster::getQueryProcessingStage(
ContextPtr context, QueryProcessingStage::Enum to_stage, const StorageSnapshotPtr &, SelectQueryInfo & query_info) const
ContextPtr context, QueryProcessingStage::Enum to_stage, const StorageSnapshotPtr &, SelectQueryInfo & /*query_info*/) const
{
auto object_storage_cluster_join_mode = context->getSettingsRef()[Setting::object_storage_cluster_join_mode];

if (object_storage_cluster_join_mode != ObjectStorageClusterJoinMode::ALLOW)
if (object_storage_cluster_join_mode == ObjectStorageClusterJoinMode::GLOBAL)
{
if (!context->getSettingsRef()[Setting::allow_experimental_analyzer])
throw Exception(ErrorCodes::NOT_IMPLEMENTED,
"object_storage_cluster_join_mode!='allow' is not supported without allow_experimental_analyzer=true");

if (object_storage_cluster_join_mode == ObjectStorageClusterJoinMode::LOCAL)
{
auto info = getQueryTreeInfo(query_info.query_tree, context);
if (info.has_join || info.has_cross_join || info.has_local_columns_in_where)
return QueryProcessingStage::Enum::FetchColumns;
}
"object_storage_cluster_join_mode=='global' is not supported without allow_experimental_analyzer=true");
}

/// Initiator executes query on remote node.
Expand Down
Loading