Pull Request Overview
- Opened on September 19, 2026
- Status Open
- Commit count 0
Total Delta
Open Days
Test Delta
How long has this pull request spent in each phase of its lifecycle?
Data pending calculation for pull request
Keep null-aware set atoms out of the nullable-key exact-count safeguard
Related: https://github.com/ClickHouse/ClickHouse/issues/90461
Changelog category (leave one):
- CI Fix or Improvement (changelog entry is not required)
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
...
Description
On a debug or sanitizer build, SELECT count() over a Nullable primary key aborts with
Logical error: 'Inconsistent KeyCondition behavior' when the filter's set holds a NULL:
Reproducer
```sql
CREATE TABLE t (x Nullable(UInt64)) ENGINE = MergeTree ORDER BY x
SETTINGS allow_nullable_key = 1, index_granularity = 1;
INSERT INTO t VALUES (1), (2), (3), (4), (NULL), (NULL), (NULL);
SELECT count() FROM t WHERE has(CAST([NULL], 'Array(Nullable(UInt64))'), x);
SELECT count() FROM t WHERE x IN (SELECT arrayJoin(CAST([NULL], 'Array(Nullable(UInt64))'))) SETTINGS transform_null_in = 1;
```
The #96844 carrier is the same shape at scale. Its `server.log` shows the atom is `nullIn` and the
table holds 48 NULL rows, written by two `divideOrNull(number, NULL)` inserts that the report's
generated reproducer drops, which is why that paste answers `0` instead of aborting.
It takes the server down mid-job, from markRangesFromPKRange. Found by
AST fuzzer (amd_debug, targeted) on #115919 @ a8e3557f
(report)
and on #96844 @ 8c888f34
(report).
Root cause. count() from the implicit exact-count projection asks KeyCondition for exact mark
ranges. An ordinary comparison is NULL for a NULL key value, so checkInHyperrectangle forces
can_be_false on every atom that answers NULL for one, except IS NULL/IS NOT NULL, which answer
definitely. Set membership comparing a NULL as an ordinary element answers definitely too
(nullIn(NULL, {NULL}) is 1; has alike). But the RPN maps in, nullIn and has onto one
FUNCTION_IN_SET, so the safeguard could not tell them apart: it overrode the set index's
already-exact verdict, and the ranges stopped matching matchesExactContinuousRange.
Change. Carry the distinction on the RPN element and skip those atoms in both
mayReadNullKeyValue overloads, but only over a bare key: a monotonic wrapper is not applied to the
NULL stand-in bound, so for a wrapped key the algebra cannot describe what the wrapper does to a NULL
row, and the safeguard stays. can_be_true is untouched, so pruning is unaffected and every count
stays the one a plain read returns.
Validation. The abort is deterministic before and silent after for nullIn, globalNullIn and
has over ascending, descending and LowCardinality(Nullable) keys; in, notIn, notHas and
NULL-free has counts are unchanged. 05083_nullable_key_exact_count commits the nullIn, has,
descending, wrapped-key and negated-set arms on both index representations, plus
EXPLAIN projections = 1 assertions that fail without the fix in a release build; the negated-set one
flips on the notNullIn flag alone. The rest was measured only.
Workflow [PR]
Sync PR [sync-upstream/pr/121089]
<!-- CI automatic block end :ci_links: -->
Comments Threads Pending Resolution
Resolved Comment Threads
No resolved comments have been left on this PR.