From ff247ec7d9722f6a9fc5afa000b13edf9540cc58 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:04:46 -0700 Subject: [PATCH] fix: coerce SIMILAR TO operands to a common string type `SIMILAR TO` panicked with "failed to downcast array" whenever its two operands resolved to arrays of different physical string types (for example a Utf8View column matched against a non-literal Utf8 pattern, or a NULL pattern producing a NullArray). Unlike LIKE and the regex operators, the TypeCoercion analyzer left `Expr::SimilarTo` in the no-op arm, so the executing kernel downcast the right array to the left array's type and panicked. A NULL pattern even panicked at plan time during constant folding. Give `SimilarTo` a dedicated coercion arm mirroring the existing `Like` arm: compute the operand types, find the common string type via `like_coercion`, and cast both operands to it (preserving the Dictionary(_, Utf8) short-circuit). This guarantees both operands reach `regex_match_dyn` as the same physical type and coerces NULL patterns into the common type, eliminating both panics. Adds a unit test next to `like_for_type_coercion` and end-to-end sqllogictest coverage for the Utf8View-vs-Utf8 and NULL-pattern cases. Fixes #22886 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../optimizer/src/analyzer/type_coercion.rs | 80 ++++++++++++++++++- .../sqllogictest/test_files/type_coercion.slt | 32 +++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/datafusion/optimizer/src/analyzer/type_coercion.rs b/datafusion/optimizer/src/analyzer/type_coercion.rs index 032fe2524096e..3ce3f9efe94a3 100644 --- a/datafusion/optimizer/src/analyzer/type_coercion.rs +++ b/datafusion/optimizer/src/analyzer/type_coercion.rs @@ -613,6 +613,33 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> { case_insensitive, )))) } + Expr::SimilarTo(Like { + negated, + expr, + pattern, + escape_char, + case_insensitive, + }) => { + let left_type = expr.get_type(self.schema)?; + let right_type = pattern.get_type(self.schema)?; + let coerced_type = like_coercion(&left_type, &right_type).ok_or_else(|| { + plan_datafusion_err!( + "There isn't a common type to coerce {left_type} and {right_type} in SIMILAR TO expression" + ) + })?; + let expr = match left_type { + DataType::Dictionary(_, inner) if *inner == DataType::Utf8 => expr, + _ => Box::new(expr.cast_to(&coerced_type, self.schema)?), + }; + let pattern = Box::new(pattern.cast_to(&coerced_type, self.schema)?); + Ok(Transformed::yes(Expr::SimilarTo(Like::new( + negated, + expr, + pattern, + escape_char, + case_insensitive, + )))) + } Expr::BinaryExpr(BinaryExpr { left, op, right }) => { let (left, right) = self.coerce_binary_op(*left, self.schema, op, *right, self.schema)?; @@ -812,7 +839,6 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> { | Expr::Column(_) | Expr::ScalarVariable(_, _) | Expr::Literal(_, _) - | Expr::SimilarTo(_) | Expr::IsNotNull(_) | Expr::IsNull(_) | Expr::Cast(_) @@ -2239,6 +2265,58 @@ mod test { Ok(()) } + #[test] + fn similar_to_for_type_coercion() -> Result<()> { + // similar to : utf8 SIMILAR TO "abc" + let expr = Box::new(col("a")); + let pattern = Box::new(lit(ScalarValue::new_utf8("abc"))); + let similar_to_expr = + Expr::SimilarTo(Like::new(false, expr, pattern, None, false)); + let empty = empty_with_type(Utf8); + let plan = + LogicalPlan::Projection(Projection::try_new(vec![similar_to_expr], empty)?); + + assert_analyzed_plan_eq!( + plan, + @r#" + Projection: a SIMILAR TO Utf8("abc") + EmptyRelation: rows=0 + "# + )?; + + // NULL pattern is coerced into the common type rather than panicking + // during constant folding (see issue #22886). + let expr = Box::new(col("a")); + let pattern = Box::new(lit(ScalarValue::Null)); + let similar_to_expr = + Expr::SimilarTo(Like::new(false, expr, pattern, None, false)); + let empty = empty_with_type(Utf8); + let plan = + LogicalPlan::Projection(Projection::try_new(vec![similar_to_expr], empty)?); + + assert_analyzed_plan_eq!( + plan, + @r" + Projection: a SIMILAR TO CAST(NULL AS Utf8) + EmptyRelation: rows=0 + " + )?; + + let expr = Box::new(col("a")); + let pattern = Box::new(lit(ScalarValue::new_utf8("abc"))); + let similar_to_expr = + Expr::SimilarTo(Like::new(false, expr, pattern, None, false)); + let empty = empty_with_type(DataType::Int64); + let plan = + LogicalPlan::Projection(Projection::try_new(vec![similar_to_expr], empty)?); + assert_type_coercion_error( + plan, + "There isn't a common type to coerce Int64 and Utf8 in SIMILAR TO expression", + )?; + + Ok(()) + } + #[test] fn unknown_for_type_coercion() -> Result<()> { // unknown diff --git a/datafusion/sqllogictest/test_files/type_coercion.slt b/datafusion/sqllogictest/test_files/type_coercion.slt index 7039e66b38b15..ba78b98205877 100644 --- a/datafusion/sqllogictest/test_files/type_coercion.slt +++ b/datafusion/sqllogictest/test_files/type_coercion.slt @@ -301,4 +301,34 @@ query error does not support zero arguments SELECT * FROM (SELECT 1) WHERE CAST(STARTS_WITH() AS STRING) = 'x'; query error does not support zero arguments -SELECT * FROM (SELECT 1) WHERE TRY_CAST(STARTS_WITH() AS INT) = 1; \ No newline at end of file +SELECT * FROM (SELECT 1) WHERE TRY_CAST(STARTS_WITH() AS INT) = 1; + +################################################################ +## SIMILAR TO operand type coercion +## https://github.com/apache/datafusion/issues/22886 +################################################################ + +statement ok +CREATE TABLE similar_to_t AS +SELECT arrow_cast(s, 'Utf8View') AS v, p FROM (VALUES + ('abc', 'abc'), + ('xyz', 'abc') +) AS t(s, p); + +# Utf8View column matched against a non-literal Utf8 pattern column used to panic +# with "failed to downcast array"; both operands now coerce to a common type. +query B +SELECT v SIMILAR TO p FROM similar_to_t ORDER BY v; +---- +true +false + +# A NULL pattern used to panic during constant folding; it now evaluates to NULL. +query B +SELECT v SIMILAR TO NULL FROM similar_to_t ORDER BY v; +---- +NULL +NULL + +statement ok +DROP TABLE similar_to_t; \ No newline at end of file