We were unable to construct the commit group for this pull request: None of the pull request's commits have been successfully processed as yet.

Keep null-aware set atoms out of the nullable-key exact-count safeguard #121089

Open
groeneai opened 8:08pm on September 19, 2026 wants to merge 0 commits into clickhouse/clickhouse master from
groeneai/keycondition-null-set-exactness

Pull Request Overview

  • Opened on September 19, 2026
  • Status Open
  • Commit count 0

Total Delta

0 Total Diff Delta

Open Days

Open 0 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Data pending calculation for pull request

Author avatar

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.