Fix arrayRemove logical error when comparing incompatible Variant types#105248
Conversation
When `arrayRemove`'s first argument is an `Array(Variant(...))` whose Variant alternatives are all incompatible with the type of the second argument AND `variant_throw_on_type_mismatch = 0`, `equals` returns `Nullable(Nothing)` instead of throwing. After `removeNullable` we therefore had a `ColumnNothing` that `arrayRemove` then wrapped under a declared type of `UInt8` and passed to the `if` resolver. `FunctionIf::executeImpl` dispatched the `UInt8/UInt8` typed paths, failed to match the actual `ColumnNothing` column, fell into `executeGeneric`, and tripped the `assertTypeEquality` chassert inside `IColumn::insertFrom` (`typeid(ColumnUInt8) != typeid(ColumnNothing)`). Detect this mismatch right after the `removeNullable`: when the column's underlying type is not `UInt8`, no real equality comparison was possible, so substitute a constant-zero `UInt8` column. The downstream `if` then computes the correct filter (no element of `arr` is considered equal to `elem`) and `arrayRemove` returns the array unchanged. Reproducer (STID 2508-1fb5, hit on PR ClickHouse#94517 since 2026-05-04): ``` SET variant_throw_on_type_mismatch = 0; SELECT arrayRemove([NULL, [257, 65537]], [['hello']]); -- before: server logical error -- after : [NULL,[257,65537]] ``` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
cc @rschu1ze @Avogar — could you review this? Fix for STID 2508-1fb5 (an |
|
Workflow [PR], commit [1fcf1c2] Summary: ✅
AI ReviewSummaryThis PR fixes Final VerdictStatus: ✅ Approve |
CI ledger — covered SHA:
|
| Check | Test | Disposition |
|---|---|---|
Unit tests (asan_ubsan) |
(check-level) | infra/chronic UBSan UntrackedMemoryCounter — task 2026-06-03-ci-p1-chronic-ubsan-in-untrackedmemoryc |
Stateless tests (amd_tsan, parallel, 1/2) |
(check-level, no test_name in CIDB) | likely the obfuscator chronic flaky pattern; no specific PR-caused failures captured. |
No PR-caused failures captured. Awaiting human review.
Session: cron:our-pr-ci-monitor:20260604-213000
…House#95839 Extend the 04256 regression test with the exact query reported on issue ClickHouse#95839 (STID 2508-1fb5): arrayRemove over a multi-alternative Array(Variant(Array(Int64), Array(Float64))) with no NULL element, paired with Array(Array(String)). This exercises the same assertTypeEquality chassert path as the existing [NULL, X] cases but via a distinct Variant-construction trigger, guarding against the regression the arrayRemove fix addresses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (commit c8f5c4e — added #95839 regression query)
Session id: cron:clickhouse-worker-slot-0:20260613-050800 |
|
CI fully finished on HEAD
The |
The recovery added for the Variant no-throw-on-mismatch case caught any non-UInt8 result from equals() and silently turned it into an all-zero filter, which would make arrayRemove quietly keep every element if equals() ever returned an unexpected type, masking a broken comparison contract. Restrict the constant-zero substitution to TypeIndex::Nothing (the documented Nullable(Nothing) result of an impossible Variant comparison with variant_throw_on_type_mismatch=0) and throw LOGICAL_ERROR for any other non-UInt8 result so a genuine contract violation surfaces instead of being hidden. Addresses clickhouse-gh[bot] review on PR ClickHouse#105248. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (commit
|
| # | Question | Answer |
|---|---|---|
| a | Deterministic repro? | Yes. With the previous broad branch removed, the bot's concern (silent all-zero filter for any non-UInt8 equals result) is reproduced by reasoning about the contract; the two known Nothing-producing repros (SET variant_throw_on_type_mismatch=0; SELECT arrayRemove([NULL,[257,65537]],[['hello']]) and SELECT arrayRemove([[9223372036854775807,-9223372036854775808],[0.9999,7]],[['hello']])) deterministically take the TypeIndex::Nothing branch. |
| b | Root cause explained? | The recovery branch was too broad: if (else_col->getDataType() != TypeIndex::UInt8) converted any non-UInt8 equals result into a constant-zero filter. Only Nullable(Nothing) (impossible Variant comparison with variant_throw_on_type_mismatch=0, stripped to ColumnNothing by removeNullable) is a legitimate recovery case; any other non-UInt8 type would be a broken equals contract that the code silently swallowed (keeping every element). |
| c | Fix matches root cause? | Yes. The substitution is now gated on else_col->getDataType() == TypeIndex::Nothing; any other non-UInt8 result throws LOGICAL_ERROR instead of being masked. This is exactly the bot's request. |
| d | Test intent preserved / new tests added? | Preserved. Existing test 04256_arrayRemove_variant_no_throw_on_mismatch (incl. the #95839 multi-alternative-Variant case) continues to exercise the Nothing recovery path; no assertions weakened. The new LOGICAL_ERROR branch is unreachable under any normal equals result, so no new test is added for it (it is a defensive contract guard, not a user-facing behavior). |
| e | Both directions demonstrated? | Yes. Built debug binary (Build ID e983a5c...). clickhouse local: the two Nothing-path repros return the array unchanged; default variant_throw_on_type_mismatch=1 throws clean ILLEGAL_TYPE_OF_ARGUMENT (no abort); ordinary arrayRemove cases unaffected. Test runner: 04256 30/30 OK with randomization; arrayRemove/assertTypeEquality/variant_throw suites 15/15 OK. |
| f | Fix is general, not a narrow patch? | Yes. This narrows a recovery branch rather than guarding a symptom: the legitimate recovery (Nothing) is handled and every other deviation now surfaces as LOGICAL_ERROR at the single point where arrayRemove consumes the equals result. No sibling arrayRemove path builds an if-filter this way. |
Session id: cron:clickhouse-worker-slot-3:20260613-131600
LLVM Coverage ReportChanged lines: Changed C/C++ lines covered by tests: 6/8 (75.00%) | Lost baseline coverage: none · Uncovered code |

Related: #95839
When
arrayRemove's first argument is anArray(Variant(...))whose Variant alternatives are all incompatible with the type of the second argument andvariant_throw_on_type_mismatch = 0,equalsreturnsNullable(Nothing)instead of throwing. The previous code stripped theNullableand then declared the resultingColumnNothingas having typeUInt8when handing it toFunctionIf. TheUInt8/UInt8typed paths insideifdid not match the actual column class, dispatch fell intoexecuteGeneric, andresult_column->insertFrom(*col_else, i)then tripped theassertTypeEqualitychassert (typeid(ColumnUInt8) != typeid(ColumnNothing)) insideIColumn::insertFrom.Detect the type mismatch right after
removeNullable: when the column's underlying type is notUInt8, no real equality comparison was possible, so substitute a constant-zeroUInt8column. The downstreamifthen yields the correct filter (no element ofarris considered equal toelem) andarrayRemovereturns the array unchanged.Reproducer (STID 2508-1fb5, hit on PR #94517 since 2026-05-04):
This also covers the multi-alternative Variant query reported on issue #95839 (verified locally: the query aborts with the STID 2508-1fb5 assertion on master and returns a clean result with this fix):
Follow-up to #101105 (which widened
assertTypeEqualityto handle wrapper columns onrhs); this PR closes the remaining call-site wherearrayRemovewas producing a column whose runtime type did not match its declared type.Discovered via @alexey-milovidov directive on #94517 (2026-05-18T15:08:20Z), STID 2508-1fb5; re-confirmed on #94517 (2026-06-13) and tracked by issue #95839.
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fix a logical error in
arrayRemovewhen the first argument is an array ofVariantwhose alternatives are all incompatible with the type of the second argument andvariant_throw_on_type_mismatchis disabled. The function now treats the comparison as "never equal" and returns the array unchanged instead of triggering a server-sideassertTypeEqualityfailure.Documentation entry for user-facing changes
Version info
26.6.1.768