Fix crash in partition pruning with nested LowCardinality constant#103211
Conversation
`KeyCondition::applyFunctionChainToColumn` used to call
`convertToFullIfNeeded` on the input column before casting it through
the monotonic function chain, while stripping only outer-level
`LowCardinality` from the type via `removeLowCardinality`.
For a constant of type `Variant(LowCardinality(Date), String)` wrapped
in `ColumnConst`, the default `IColumn::convertToFullIfNeeded`
implementation bypasses `ColumnVariant`'s own override (which
deliberately avoids recursing into variant sub-columns) and strips
`LowCardinality` from every variant sub-column. The type meanwhile
keeps its nested `LowCardinality`. The subsequent
`castColumnAccurateOrNull` eventually dispatches into
`FunctionCast::prepareUnpackDictionaries`, which asserts a
`ColumnLowCardinality` but sees the plain `ColumnVector`, aborting the
server with `LOGICAL_ERROR`:
Bad cast from type DB::ColumnVector<...> to DB::ColumnLowCardinality
Replace `convertToFullIfNeeded` with explicit outer-level
`convertToFullColumnIfConst`/`Replicated`/`Sparse`/`LowCardinality`
chain so that the column stripping matches `removeLowCardinality` on the
type. Nested `LowCardinality` inside container types (`Array`, `Tuple`,
`Map`, `Variant`) is preserved on both sides, so subsequent casts stay
consistent and `FunctionCast` handles any remaining `LowCardinality`
internally through its own wrappers.
The crash is reached via `PartitionPruner` / `MergeTreeIndexMinMax`
during index analysis and reproduces deterministically with a filter
that casts a constant to a `Variant` containing nested `LowCardinality`
over a table with `LowCardinality` key. CIDB shows 11 hits over 90 days
on `serverfuzz` / `Stress test` / `AST fuzzer` checks covering
`PartitionPruner` and `MergeTreeIndexMinMax` paths (STID 4054-600e).
Reported in CI: STID 4054-600e on master `Stress test (experimental,
serverfuzz, amd_tsan)` at commit 728336b
Pre-PR validation gatea) Deterministic repro? Yes. SET allow_experimental_variant_type=1;
SET allow_suspicious_low_cardinality_types=1;
CREATE TABLE t (d LowCardinality(Date), v UInt32) ENGINE=MergeTree
PARTITION BY toYYYYMM(d) ORDER BY d SETTINGS allow_suspicious_low_cardinality_types=1;
INSERT INTO t VALUES ('2026-04-01', 1), ('2026-04-19', 2), ('2026-04-20', 3);
SELECT * FROM t WHERE d >= CAST('2026-04-19' AS Variant(LowCardinality(Date), String));Aborts every run before the fix; returns correct rows after the fix. b) Root cause explained? Yes.
c) Fix matches root cause? Yes — replace the asymmetric call with an outer-only strip that matches what d) Test intent preserved / new test added? Yes — no existing test assertions are weakened. New regression test e) Demonstrated in both directions? Yes.
f) Fix is general, not a narrow patch? Yes.
Session: |
|
Workflow [PR], commit [5a11f97] Summary: ❌
AI ReviewSummaryThis PR fixes a real correctness issue in ClickHouse Rules
Final Verdict
|
|
@groeneai Failed stateless tests look related. |
The regression test for STID 4054-600e uses `PARTITION BY toYYYYMM(d)` on a `LowCardinality(Date)` column and casts a constant to `Variant(LowCardinality(Date), String)`. Under parallel replicas the query tree is serialized with the `Date` constant folded to its raw `UInt16` representation and re-parsed on the replica, where `FunctionCast::createColumnToVariantWrapper` rejects `UInt16 -> Variant(LowCardinality(Date), String)` with `CANNOT_CONVERT_TYPE` before index analysis runs. That is a separate, pre-existing limitation in parallel replicas constant serialization, unrelated to the `KeyCondition` fix this PR targets. Skip the parallel-replicas variants so the test exclusively exercises the single-server `KeyCondition::applyFunctionChainToColumn` path that the fix addresses. Remove the secondary `LowCardinality(String)` case which doesn't reach `canConstantBeWrappedByMonotonicFunctions` through its ORDER-BY-only schema and so was not actually exercising the fix. Verified locally: reproduces `Bad cast … to ColumnLowCardinality` without the `KeyCondition` change, returns the expected rows with it. Reviewer feedback: ClickHouse#103211 (comment)
|
Thanks @nihalzp — you were right, the Root cause of the CI failures: the new test exercises partition pruning over Under parallel replicas / distributed plan the query tree is serialized with the I verified this is a pre-existing parallel-replicas serialization limitation (not introduced by this fix) by reproducing the same SELECT d, v FROM remote('127.0.0.1:19019', currentDatabase(), 't_04105_lc_date')
WHERE d >= CAST('2026-04-19' AS Variant(LowCardinality(Date), String))
ORDER BY d;
-- Code: 70. Cannot convert type UInt16 to Variant(LowCardinality(Date), String).Fixing the analyzer's Variant constant serialization is out of scope for this PR. Similar MergeTree index-analysis tests use the same tag (e.g. The other five LLVM-coverage stateless failures ( |
LLVM Coverage Report
Changed lines: 100.00% (6/6) · Uncovered code |
|
Gentle re-ping @CurtizJ @KochetovNicolai — could you take a look when you have a moment? Quick status:
Happy to rebase or tweak if anything needs changing. |
d95da10
|
Hi @groeneai @nihalzp — the changelog category for this PR might need updating. Current: Why: The PR fixes a LOGICAL_ERROR ('Bad cast from ColumnVector to ColumnLowCardinality'), which belongs in the 'Critical Bug Fix' category according to the changelog guidelines. Could you verify this is correct? Ignore if the current category is intentional. |

Fixes a
LOGICAL_ERRORserver exception during MergeTree index analysis when a constant in aWHEREclause is cast to a type containing nestedLowCardinality(for example, aVariant(LowCardinality(Date), String)).CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?REF=master&sha=728336b344ee2de5aaace19f7a67079b3070d03c&name_0=MasterCI&name_1=Stress%20test%20%28experimental%2C%20serverfuzz%2C%20amd_tsan%29 (STID 4054-600e)
Root cause
KeyCondition::applyFunctionChainToColumnprepared the input constant by callingconvertToFullIfNeededon the column andremoveLowCardinalityon the type. The two were not symmetric:removeLowCardinalityonly strips the outer-levelLowCardinalitywrapper from the type.IColumn::convertToFullIfNeededimplementation recurses into sub-columns viaforEachSubcolumnand stripsLowCardinalityfrom every nested sub-column.ColumnVariantoverridesconvertToFullIfNeededto a no-op precisely because there is no way to keep theDataTypeVariantin sync with per-variant column substitutions. But when theColumnVariantis wrapped in aColumnConst(which is whatcanConstantBeWrappedByMonotonicFunctionsproduces viacreateColumnConst), the default implementation onColumnConstis used and bypasses theColumnVariantoverride — strippingLowCardinalityfrom the variant sub-columns while the declaredDataTypeVariantis left untouched byremoveLowCardinality.The subsequent
castColumnAccurate/castColumnAccurateOrNullbuilt aFunctionCastfrom the mismatchedVariant(LowCardinality(X), ...)type. InsidecreateVariantToColumnWrapper, the per-variant wrapper wasprepareUnpackDictionaries(LowCardinality(X), ...), whichtypeid_casts its input toColumnLowCardinality. At runtime the sub-column was a plainColumnVector, so the server aborted with:The same asymmetry is triggered by
Array(LowCardinality(X)),Tuple(LowCardinality(X), ...), andMap(LowCardinality(K), V)constants, but those paths are currently reachable only through the server-side AST fuzzer.Fix
Strip only outer-level wrappers on the column so that the transformation is symmetric with what
removeLowCardinalitydoes on the type:Nested
LowCardinalityinside container types (Array,Tuple,Map,Variant) is preserved on both sides. Downstream code is unaffected:FunctionCastalready handles outer-level and nestedLowCardinalityvia its own wrappers, and the existing defensive re-wrap at thefunc->executecall site continues to handle the outer-LC case.Reproducer
Bad cast from type DB::ColumnVector<unsigned short> to DB::ColumnLowCardinality.(2026-04-19, 2)and(2026-04-20, 3).CIDB history
STID 4054-600e: 11 hits over 90 days across
serverfuzz/Stress test/AST fuzzerchecks, all going throughPartitionPruner(7/11) orMergeTreeIndexMinMax(4/11) and ending inFunctionCast::prepareUnpackDictionaries. No open GitHub issue tracks this STID specifically.Test
Added
tests/queries/0_stateless/04105_key_condition_nested_lc_variant.sqlcovering theVariant(LowCardinality(Date), String)partition-pruning path and theVariant(LowCardinality(String), UInt64)primary-key path.Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fix
LOGICAL_ERROR(Bad cast … to ColumnLowCardinality) during MergeTree index analysis when aWHEREclause compares aLowCardinalitykey column with a constant cast to a type that contains nestedLowCardinality(for exampleVariant(LowCardinality(Date), String)). #NNNNNDocumentation entry for user-facing changes
Session:
cron:clickhouse-ci-task-worker:20260421-001500Version info
26.5.1.26