From c52eb9734b4c968ad9880fb82f3214efd88cd82b Mon Sep 17 00:00:00 2001 From: Patrick Ribbsaeter Date: Thu, 15 Oct 2026 12:48:50 +0000 Subject: [PATCH] fix: downcast to FixedSizeBinaryArray in CASE literal lookup table (#26096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Which issue does this PR close? Closes #26012. ## Rationale for this change In `LiteralLookupTable`, evaluation of `CASE WHEN ...` with byte-like literals delegates to `BytesLikeIndexMap`. In `try_get_bytes_iterator`, the `DataType::FixedSizeBinary(_)` match arm erroneously attempted to downcast the array via `as_binary::()` (inherited from `DataType::LargeBinary`), rather than handling `FixedSizeBinaryArray`. When a query contained 2 or more `FixedSizeBinary` WHEN literals, `LiteralLookupTable` was constructed and panicked during evaluation when downcasting to `GenericBinaryArray`: ```text thread 'main' panicked at arrow-array-60.0.0/src/cast.rs:938:29: binary array ``` ## What changes are included in this PR? 1. **Fix Downcast in Lookup Table**: Updated `DataType::FixedSizeBinary(_)` arm in `try_get_bytes_iterator` (`bytes_like_lookup_table.rs`) to downcast via `array.as_fixed_size_binary().iter()`, matching the dictionary downcasting path and returning byte slices correctly. 2. **Unit Test**: Added `test_case_when_literal_lookup_fixed_size_binary_to_int32` in `datafusion/physical-expr/src/expressions/case.rs` testing `LiteralLookupTable` evaluation with `FixedSizeBinary` literals with and without ELSE, with and without NULLs. 3. **Sqllogictest**: Added regression tests in `datafusion/sqllogictest/test_files/case.slt` verifying `FixedSizeBinary` `CASE` evaluation against branch 1, branch 2, fallback, NULL inputs, and multiple rows. ## Are these changes tested? Yes: - Unit test in `datafusion/physical-expr/src/expressions/case.rs` (`test_case_when_literal_lookup_fixed_size_binary_to_int32`) - Sqllogictest in `datafusion/sqllogictest/test_files/case.slt` - Validated locally with `cargo test` and `cargo clippy`. ## Are there any user-facing changes? Bug fix: queries with `CASE` expressions containing multiple `FixedSizeBinary` literals now evaluate correctly instead of panicking. --- *Diff minimization and pre-flight validation performed with [ZeroSlop™ CI](https://github.com/marketplace/actions/zeroslop-by-ribbsaeter-systems) — [Ribbsaeter Systems](https://ribbsaetersystems.com/).* Signed-off-by: Patrick Ribbsaeter --- .../physical-expr/src/expressions/case.rs | 46 ++++++++++++++ .../bytes_like_lookup_table.rs | 2 +- datafusion/sqllogictest/test_files/case.slt | 60 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/datafusion/physical-expr/src/expressions/case.rs b/datafusion/physical-expr/src/expressions/case.rs index 1e4d106..c30eb9d 100644 --- a/datafusion/physical-expr/src/expressions/case.rs +++ b/datafusion/physical-expr/src/expressions/case.rs @@ -3500,6 +3500,52 @@ mod tests { ); } + #[test] + fn test_case_when_literal_lookup_fixed_size_binary_to_int32() { + let lookup_map = vec![ + ( + ScalarValue::FixedSizeBinary(1, Some(vec![1])), + ScalarValue::Int32(Some(10)), + ), + ( + ScalarValue::FixedSizeBinary(1, Some(vec![2])), + ScalarValue::Int32(Some(20)), + ), + ]; + + let input_values = ScalarValue::iter_to_array(vec![ + ScalarValue::FixedSizeBinary(1, Some(vec![1])), + ScalarValue::FixedSizeBinary(1, Some(vec![2])), + ScalarValue::FixedSizeBinary(1, Some(vec![3])), + ScalarValue::FixedSizeBinary(1, None), + ]) + .expect("valid fixed size binary array"); + + let expected_no_else = + Arc::new(Int32Array::from(vec![Some(10), Some(20), None, None])) as ArrayRef; + + test_case_when_literal_lookup( + Arc::clone(&input_values), + &lookup_map, + None, + expected_no_else, + ); + + let expected_with_else = Arc::new(Int32Array::from(vec![ + Some(10), + Some(20), + Some(99), + Some(99), + ])) as ArrayRef; + + test_case_when_literal_lookup( + input_values, + &lookup_map, + Some(ScalarValue::Int32(Some(99))), + expected_with_else, + ); + } + /// Reproduces https://github.com/apache/datafusion/issues/22173 /// /// Nested self-referential CASE chains (common in rewrite-style projections) diff --git a/datafusion/physical-expr/src/expressions/case/literal_lookup_table/bytes_like_lookup_table.rs b/datafusion/physical-expr/src/expressions/case/literal_lookup_table/bytes_like_lookup_table.rs index e5cf3f8..658d9cd 100644 --- a/datafusion/physical-expr/src/expressions/case/literal_lookup_table/bytes_like_lookup_table.rs +++ b/datafusion/physical-expr/src/expressions/case/literal_lookup_table/bytes_like_lookup_table.rs @@ -106,7 +106,7 @@ fn try_get_bytes_iterator( DataType::LargeBinary => Box::new(array.as_binary::().into_iter()), - DataType::FixedSizeBinary(_) => Box::new(array.as_binary::().into_iter()), + DataType::FixedSizeBinary(_) => Box::new(array.as_fixed_size_binary().iter()), DataType::Utf8View => Box::new( array diff --git a/datafusion/sqllogictest/test_files/case.slt b/datafusion/sqllogictest/test_files/case.slt index 3f9058b..712981e 100644 --- a/datafusion/sqllogictest/test_files/case.slt +++ b/datafusion/sqllogictest/test_files/case.slt @@ -846,6 +846,66 @@ drop table letters; statement ok drop table letters_binary; +# Regression test for https://github.com/apache/datafusion/issues/26012 +# CASE with two or more FixedSizeBinary WHEN literals +query I +SELECT CASE x + WHEN arrow_cast(X'01', 'FixedSizeBinary(1)') THEN 1 + WHEN arrow_cast(X'02', 'FixedSizeBinary(1)') THEN 2 + ELSE 3 + END AS r +FROM (SELECT arrow_cast(X'03', 'FixedSizeBinary(1)') AS x); +---- +3 + +query I +SELECT CASE x + WHEN arrow_cast(X'01', 'FixedSizeBinary(1)') THEN 1 + WHEN arrow_cast(X'02', 'FixedSizeBinary(1)') THEN 2 + ELSE 3 + END AS r +FROM (SELECT arrow_cast(X'02', 'FixedSizeBinary(1)') AS x); +---- +2 + +query I +SELECT CASE x + WHEN arrow_cast(X'01', 'FixedSizeBinary(1)') THEN 1 + WHEN arrow_cast(X'02', 'FixedSizeBinary(1)') THEN 2 + ELSE 3 + END AS r +FROM (SELECT arrow_cast(X'01', 'FixedSizeBinary(1)') AS x); +---- +1 + +query I +SELECT CASE x + WHEN arrow_cast(X'01', 'FixedSizeBinary(1)') THEN 1 + WHEN arrow_cast(X'02', 'FixedSizeBinary(1)') THEN 2 + ELSE 3 + END AS r +FROM (SELECT arrow_cast(NULL, 'FixedSizeBinary(1)') AS x); +---- +3 + +query I +SELECT CASE x + WHEN arrow_cast(X'01', 'FixedSizeBinary(1)') THEN 10 + WHEN arrow_cast(X'02', 'FixedSizeBinary(1)') THEN 20 + ELSE 99 + END AS r +FROM (VALUES + (arrow_cast(X'01', 'FixedSizeBinary(1)')), + (arrow_cast(X'02', 'FixedSizeBinary(1)')), + (arrow_cast(X'03', 'FixedSizeBinary(1)')), + (arrow_cast(NULL, 'FixedSizeBinary(1)')) +) t(x); +---- +10 +20 +99 +99 + # Tests for CASE with boolean expressions statement ok create table booleans (b boolean) as values (true), (false), (null), (true), (null), (false); -- 2.55.0.windows.3