Skip to content

Add object_storage_cluster_join_mode='distributed' for whole-query … - #2383

Open
VighneshPath wants to merge 4 commits into
Altinity:antalya-26.6from
VighneshPath:feature/antalya-26.6/object-storage-cluster-distributed-join
Open

VighneshPath wants to merge 4 commits into
Altinity:antalya-26.6from
VighneshPath:feature/antalya-26.6/object-storage-cluster-distributed-join

Conversation

@VighneshPath

Copy link
Copy Markdown

…JOIN dispatch

Introduces whole-query dispatch for JOINs involving a DataLake-catalog table backed by object_storage_cluster: findDistributedObjectStorageCandidate() walks the left spine from the root query to find such a driver table where every other table reachable in the same JOIN/subquery tree also safely resolves through a DataLake catalog, and buildDistributedObjectStorageQueryPlan() rewrites the driver's table expression into a cluster table function via IStorageCluster::buildClusterTableFunctionAST() (using QueryTree::cloneAndReplace(), not in-place mutation) and dispatches the whole enclosing query to the driver's cluster nodes.

IStorageCluster::readPreparedClusterQuery() and ReadFromCluster gained an is_whole_query_dispatch mode: driver-side filter/task-iterator pruning is suppressed (safe fallback, since the shipped query already carries its own WHERE), and query_info's table_expression/planner_context -- which describe a single table for an ordinary per-table read -- are reset before ReadFromCluster is constructed, since they otherwise get consulted by ReadFromCluster::applyFilters() as if they described the whole dispatched query, corrupting filter pushdown and, if the resulting exception path is ever hit, dereferencing a null table_expression.

Verified against a live Iceberg/MinIO-backed cluster with q17- and q21-shaped JOIN queries: correct, disjoint fan-out results across cluster nodes, filter pushdown into ReadFromCluster survives real query plan optimization, no crash.

Changelog category (leave one):

  • New Feature
  • Experimental Feature
  • Improvement
  • Performance Improvement
  • Backward Incompatible Change
  • Build/Testing/Packaging Improvement
  • Documentation (changelog entry is not required)
  • Critical Bug Fix (crash, data loss, RBAC)
  • Bug Fix (user-visible misbehavior in an official stable release)
  • CI Fix or Improvement (changelog entry is not required)
  • Not for changelog (changelog entry is not required)

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

...

Documentation entry for user-facing changes

...

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Unit tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • CAS (content-addressed storage; Antalya only)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

VighneshPath and others added 3 commits September 16, 2026 15:33
…JOIN dispatch

When a JOIN's driving table is a `DataLake` catalog table distributed via
`object_storage_cluster`, send the whole query to that table's cluster and merge
the partial aggregates on the initiator, instead of reading every table back to
the initiator and joining there. The JOIN and any `GROUP BY` then run on every
node of the cluster rather than on one.

`findDistributedObjectStorageCandidate` decides eligibility. It walks down the
left side of the join tree to find the driving table, passing through a subquery
only when that subquery does not itself aggregate, deduplicate, sort or limit --
each worker runs it against its own slice of the driver, so anything that
finalizes across rows would turn a partial result into a final one. Every other
table reachable in the query must resolve through a `DataLake` catalog, carry no
row policy, and be readable by the current user.

`buildDistributedObjectStorageQueryPlan` then replaces the driver with an
explicit `*Cluster()` table function, keyed on the exact query tree node, and
reads the result back at `WithMergeableState` through a single `ReadFromCluster`
step, so the planner's ordinary finalization applies on top. It follows
`buildQueryPlanForParallelReplicas` step for step, including the position-based
conversion from the rewritten query's header back to the original's.

Only the driving table is partitioned across the cluster; every other table in
the query is read and recomputed in full on each node. That cost is not
estimated when deciding to dispatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: VighneshPath <pathrikarvighnesh@gmail.com>
Reuse mechanisms that already exist rather than adding parallel ones:
`DatabaseCatalog::isDatalakeCatalog` in place of a per-storage marker,
`StorageObjectStorageCluster::getClusterName` in place of a second
worker-localization path in `DatabaseDataLake`, and
`SourceStepWithFilterBase::applyFilters` in place of clearing fields on the
`SelectQueryInfo` handed to `ReadFromCluster`. `DatabaseDataLake.cpp` is no
longer touched by this feature at all.

The setting's documentation described a search for the highest eligible
enclosing query, and a fallback for the level an ineligible table appears at.
Neither exists: dispatch is attempted only for the outermost `SELECT` of an
initial query, and is all-or-nothing. It now says so, and states that only the
driving table is partitioned while the rest is recomputed in full per node.

`allWorkerLocalReferencesAreSafe` is renamed to
`allWorkerLocalTableReferencesAreSafe`, because it proves nothing about ordinary
functions: `dictGet`, a user-defined function or `hostName` move from the
initiator to the workers unexamined, as they do for `Distributed`.

Tests cover both shapes against a real `DataLake` catalog -- the driver as the
JOIN's leftmost table, and the driver behind a subquery with the aggregation on
the enclosing query and a nested JOIN on the right -- in derived-table and CTE
spellings, comparing results against `object_storage_cluster_join_mode='allow'`.
They assert that the whole query reaches a worker, that only the driver becomes
a cluster function, and that no partner table fans out again from a worker. A
local `Memory` JOIN partner must fall back to ordinary planning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: VighneshPath <pathrikarvighnesh@gmail.com>
…cluster-distributed-join

Signed-off-by: VighneshPath <pathrikarvighnesh@gmail.com>
@VighneshPath
VighneshPath force-pushed the feature/antalya-26.6/object-storage-cluster-distributed-join branch from 037f342 to 10f70a2 Compare September 16, 2026 10:06
@filimonov

Copy link
Copy Markdown
Member

@ianton-ru

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.

2 participants