Skip sharding key IN rewrite when the sharding key contains a set - #110463
Skip sharding key IN rewrite when the sharding key contains a set#110463groeneai wants to merge 10 commits into
Conversation
OptimizeShardingKeyRewriteIn executes the standalone sharding-key ExpressionActions on constant values to prune shards. When the sharding key itself contains an IN/subquery (a set), that set is never populated during planning, so the execution hits FunctionIn with an unbuilt set and throws "Not-ready Set is passed as the second argument for function 'in'" (LOGICAL_ERROR, aborts in debug/sanitizer builds). Skip the rewrite when the sharding key expression contains a set and query all shards instead, the same safe fallback already used for non-deterministic sharding keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-1:20260714-220600 |
|
cc @azat @devcrafter — could you review? This skips the |
|
Workflow [PR], commit [8440d3b] Summary: ❌
|
…nalyzer path Address review on the shard-pruning fix: 1. The previous guard bailed on every DataTypeSet node in the sharding key, so shard pruning regressed for safe constant-set keys like bitAnd(dummy + (0 IN (1, 2)), 1). Tuple/storage sets are materialized eagerly during planning (FutureSet::get() is non-null); only subquery-backed sets stay unbuilt. Bail out only for the latter (shardingKeyExpressionContainsNotReadySet), keeping the optimization for ready sets. 2. The guard lived only on the analyzer rewrite path in ClusterProxy. When allow_experimental_analyzer = 0, StorageDistributed::skipUnusedShards still runs evaluateExpressionOverConstantCondition on the sharding key, which ends in target_expr->execute and hits FunctionIn with the unbuilt set (Not-ready Set logical error) before executeQuery is reached. Guard both skipUnusedShards and skipUnusedShardsWithAnalyzer with the same fallback. The helper moved to OptimizeShardingKeyRewriteInVisitor so both call sites share it. Regression test extended to cover the old-analyzer path and the ready tuple-set key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Both addressed in e6b9b24.
Verified locally: with |
# Conflicts: # ci/jobs/scripts/workflow_hooks/check_report_messages.py # tests/queries/0_stateless/04402_with_fill_interpolate_fill_column_overlap.sql
Address review: the test only distinguished exit 134 (SIGABRT), so an unfixed release build that throws Not-ready Set (non-134 exit) took the same OK branch as the intended fake-shard network error, and the ready tuple cases passed even if the helper regressed to always-fallback. Unready-set cases now assert the query gets PAST planning: fail on exit 134 (debug/sanitizer abort) OR Not-ready Set in stderr (release throw). Ready-tuple cases assert shard pruning is preserved by querying values that map to shard 1 (127.0.0.2); a regression to always-fallback would contact shard 0 (127.0.0.1) first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…in Fast test The 127.0.0.x fake shards are a live server in the single-node Fast test box, so the ready-tuple query succeeded with empty stderr and the shard-pruning discriminator could not see which shard was contacted. Switch to RFC5737 TEST-NET addresses (192.0.2.x), unreachable in every environment, and add a low failover connect timeout so the connection-error host is always observable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI finish ledger — 2e65b38Every failure below has an owner. Only
The Session id: cron:our-pr-ci-monitor:20260715-090000 |
An AI review comment suggested the guard misses lambda-based sharding keys, on the grounds that a lambda body lives in a nested DAG that shardingKeyExpressionContainsNotReadySet does not walk. The nesting premise is right but the conclusion does not follow. The IN right-hand side is not part of the lambda body DAG: ActionsVisitor creates the ColumnConst(ColumnSet) through ScopeStack::addColumn, which always adds it to stack[0] (the outer DAG) and only projects an input into the inner levels. So the set node is visible to the existing outer scan at any lambda nesting depth, and it is reported as unready there. Measured on this branch with an instrumented build: for arrayExists(x -> x IN (SELECT 1), [dummy]) and for the doubly nested variant the outer DAG has exactly one set-typed node and it is classified unready, on both the analyzer and the old-analyzer path. Dropping the guard makes the old-analyzer lambda cases abort with Not-ready Set, so the shapes are genuinely reachable and worth pinning. Add them to the test rather than changing the guard: two lambda cases and two doubly nested cases on the unready side, plus two lambda cases holding a ready tuple set on the pruning side so an over-broad bail would be caught too.
The unready-set cases read any exit other than 134 without the Not-ready Set text as a successful fallback, so an unrelated planning failure that never reaches a shard would also pass. Require stderr to name one of the configured TEST-NET hosts, which only happens once planning finished and remote execution started, and report NO-SHARD-CONTACTED otherwise. Also correct the lambda comments. They claimed the outer-DAG scan misses a set inside a lambda body; it does not, because ScopeStack::addColumn puts the ColumnConst(ColumnSet) in the outermost DAG and only projects an input into the nested scopes.
Two of the unready-set cases passed on an unfixed build, so they pinned nothing. Measured against the pristine master snapshot, the analyzer plus lambda-key rows both printed the expected marker without the fix. The reason is that the two guarded paths fail differently. On the old analyzer the sharding key expression is executed and FunctionIn throws, so the query dies and the previous "did we get past planning" assertion held. On the analyzer path nothing throws: the unbuilt set is read as a constant, the key is const-folded to a single value and the query is pruned to one shard, so rows on the other shard are dropped with no error at all. Getting past planning is therefore not evidence of a correct fallback. Assert the fallback itself instead. With skip_unavailable_shards the first unreachable shard no longer aborts the query, so every shard the planner selected reports its own connection failure and the whole selected set is visible in stderr; requiring both configured hosts distinguishes the all-shards fallback from a single-shard prune. The abort and throw shapes are still rejected explicitly. Measured on the pristine master snapshot, all six unready rows now fail (three PARTIAL-SHARDS on the analyzer path, three NOT-READY-SET-LEAK on the old analyzer) and all six pass with the fix. The oracle for the shard sets: with 0 IN (SELECT 1) reading as unbuilt, a key of arrayExists(x -> x IN (SELECT 1), [dummy]) requires both shards for dummy IN (1, 3), while master selects only one.
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 37/40 (92.50%) · Uncovered code |
CI finish ledger - ff79dd8Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task whose fixing-PR link will be posted here when it opens. Only
Session id: cron:our-pr-ci-monitor:20260729-170000 |
CI finish ledger — ff79dd8Every failure below has an owner: a fixing PR (ours or external), or a full-effort fix task whose
Session id: cron:our-pr-ci-monitor:20260729-164745 |
azat
left a comment
There was a problem hiding this comment.
Why the set is not ready?
This patch is no good
|
The set is permanently unready, by construction. The sharding key is table metadata, not part of a query. It becomes a standalone So it is not "not ready yet". A subquery in a sharding key can never become ready. You are right that this patch is no good. It guards one consumer of a state that should not exist, and it is also incomplete: the INSERT selector still aborts on this branch. Measured on head CREATE TABLE loc (id UInt64) ENGINE = MergeTree ORDER BY id;
-- `two_shards`: any cluster with 2 shards
CREATE TABLE dsub (id UInt64) ENGINE = Distributed(two_shards, currentDatabase(), loc,
bitAnd(id + (0 IN (SELECT 1)), 1));
INSERT INTO dsub SELECT number FROM numbers(4);aborts with What I would do instead: reject a subquery-backed set in the sharding key where the key is validated, in I will rework the PR that way unless you would rather the check live somewhere else. |
|
Following up on the root cause above, with a design question I would rather ask than guess at. Rejecting this at DDL time splits into two designs, and the split is structural rather than a matter of taste. A sharding key CREATE TABLE ms (id UInt64) ENGINE = Set; -- INSERT INTO dist ... works
CREATE TABLE loc (id UInt64) ENGINE = MergeTree ORDER BY id; -- INSERT aborts, Not-ready SetSo: A. Syntactic check. Reuse B. Exact check. Reject only when B keeps Which would you prefer? I will also fold in the |
Related: #107619
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fix the
Not-ready Set is passed as the second argument for function 'in'logical error that could be thrown when aDistributedtable's sharding key expression itself contained anIN/subquery (a set) andoptimize_skip_unused_shards_rewrite_inwas enabled.Description
Found by the AST fuzzer (STID
0250-4e52, tracked in #107619). Reported on the arm_debug Stress test of PR #101039; per @ alexey-milovidov the failure is unrelated to that PR, so this is a separate fix.Reproducer (aborts in debug/sanitizer builds):
CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=101039&sha=8b127de6068682ddc50262e918ab1a7dd3a0a307&name_0=PR&name_1=Stress%20test%20%28arm_debug%29
Root cause:
OptimizeShardingKeyRewriteInprunes shards by executing the standalone sharding-keyExpressionActionson constant values (shardContains->executeFunctionOnField). The sharding-key expression is built directly from the sharding-key AST, so anIN/subquery in the sharding key becomes aFunctionInwhose set is never populated during planning. Executing it hitsFunctionIn::executeImplwith an unbuilt set and throws theNot-ready Setlogical error (OptimizeShardingKeyRewriteInVisitor.cpp:36->Functions/in.cpp:116).Fix: skip the rewrite when the sharding-key expression contains a set, and query all shards instead. This is the same safe fallback already used for non-deterministic sharding keys, and the optimization was never valid for a set-containing key anyway (it cannot be constant-folded per value). Both the analyzer and old-analyzer call sites are guarded.
Distinct from the other in-flight
Not-ready Setfixes (#109722, #102192), which address thebuildOrderedSetInplace/QueryPlanpath; neither touchesOptimizeShardingKeyRewriteIn.