-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix: coerce SIMILAR TO operands to a common string type to avoid 'failed to downcast array' panic #22887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: coerce SIMILAR TO operands to a common string type to avoid 'failed to downcast array' panic #22887
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,7 @@ use datafusion_expr::expr_rewriter::coerce_plan_expr_for_schema; | |
| use datafusion_expr::expr_schema::cast_subquery; | ||
| use datafusion_expr::logical_plan::Subquery; | ||
| use datafusion_expr::type_coercion::binary::{ | ||
| comparison_coercion, like_coercion, type_union_coercion, | ||
| comparison_coercion, like_coercion, regex_coercion, type_union_coercion, | ||
| }; | ||
| use datafusion_expr::type_coercion::functions::{ | ||
| UDFCoercionExt, fields_with_udf, value_fields_with_higher_order_udf_and_lambdas, | ||
|
|
@@ -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 = regex_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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems a bit odd here to limit it only to |
||
| _ => 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,75 @@ mod test { | |
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn similar_to_for_type_coercion() -> Result<()> { | ||
| // utf8 SIMILAR TO utf8 needs no coercion | ||
| 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 | ||
| "# | ||
| )?; | ||
|
|
||
| // utf8 SIMILAR TO NULL casts the pattern to utf8 | ||
| 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 | ||
| " | ||
| )?; | ||
|
|
||
| // utf8view SIMILAR TO utf8 casts the pattern to utf8view | ||
| 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::Utf8View); | ||
| let plan = | ||
| LogicalPlan::Projection(Projection::try_new(vec![similar_to_expr], empty)?); | ||
|
|
||
| assert_analyzed_plan_eq!( | ||
| plan, | ||
| @r#" | ||
| Projection: a SIMILAR TO CAST(Utf8("abc") AS Utf8View) | ||
| EmptyRelation: rows=0 | ||
| "# | ||
| )?; | ||
|
|
||
| // non-string operands are rejected | ||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,11 +25,12 @@ use arrow::compute::kernels::bitwise::{ | |
| bitwise_xor, bitwise_xor_scalar, | ||
| }; | ||
| use arrow::compute::kernels::boolean::not; | ||
| use arrow::compute::kernels::cast::cast; | ||
| use arrow::compute::kernels::comparison::{regexp_is_match, regexp_is_match_scalar}; | ||
| use arrow::datatypes::DataType; | ||
| use arrow::error::ArrowError; | ||
| use datafusion_common::{Result, ScalarValue}; | ||
| use datafusion_common::{internal_err, plan_err}; | ||
| use datafusion_common::{downcast_value, internal_err, plan_err}; | ||
|
|
||
| use std::sync::Arc; | ||
|
|
||
|
|
@@ -251,14 +252,8 @@ pub fn concat_elements_binary_view_array( | |
| /// Invoke a compute kernel on a pair of binary data arrays with flags | ||
| macro_rules! regexp_is_match_flag { | ||
| ($LEFT:expr, $RIGHT:expr, $ARRAYTYPE:ident, $NOT:expr, $FLAG:expr) => {{ | ||
| let ll = $LEFT | ||
| .as_any() | ||
| .downcast_ref::<$ARRAYTYPE>() | ||
| .expect("failed to downcast array"); | ||
| let rr = $RIGHT | ||
| .as_any() | ||
| .downcast_ref::<$ARRAYTYPE>() | ||
| .expect("failed to downcast array"); | ||
| let ll = downcast_value!($LEFT, $ARRAYTYPE); | ||
| let rr = downcast_value!($RIGHT, $ARRAYTYPE); | ||
|
|
||
| let flag = if $FLAG { | ||
| Some($ARRAYTYPE::from(vec!["i"; ll.len()])) | ||
|
|
@@ -273,12 +268,37 @@ macro_rules! regexp_is_match_flag { | |
| }}; | ||
| } | ||
|
|
||
| /// Unpack a dictionary array to its value type, leaving other arrays as-is | ||
| fn flatten_dictionary(array: &ArrayRef) -> Result<ArrayRef> { | ||
| match array.data_type() { | ||
| DataType::Dictionary(_, value_type) => Ok(cast(array, value_type)?), | ||
| _ => Ok(Arc::clone(array)), | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn regex_match_dyn( | ||
| left: &ArrayRef, | ||
| right: &ArrayRef, | ||
| not_match: bool, | ||
| flag: bool, | ||
| ) -> Result<ArrayRef> { | ||
| // Type coercion deliberately preserves `Dictionary(_, Utf8)` inputs so | ||
| // that literal patterns can use the dictionary fast path in | ||
| // `regex_match_dyn_scalar`. Here every row has its own pattern, so the | ||
| // dictionary encoding offers no shortcut: flatten the inputs, and if the | ||
| // dictionary's value type differs from the coerced pattern type (e.g. | ||
| // `Dictionary(Int32, Utf8)` matched against `Utf8View` patterns), cast to | ||
| // a common string type. | ||
| if matches!(left.data_type(), DataType::Dictionary(_, _)) | ||
| || matches!(right.data_type(), DataType::Dictionary(_, _)) | ||
| { | ||
| let left = flatten_dictionary(left)?; | ||
| let mut right = flatten_dictionary(right)?; | ||
| if right.data_type() != left.data_type() { | ||
| right = cast(&right, left.data_type())?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the left side is We can cast the dictionary side to the already-coerced pattern type, or compute the common string type again. |
||
| } | ||
| return regex_match_dyn(&left, &right, not_match, flag); | ||
| } | ||
| match left.data_type() { | ||
| DataType::Utf8 => { | ||
| regexp_is_match_flag!(left, right, StringArray, not_match, flag) | ||
|
|
@@ -299,27 +319,32 @@ pub(crate) fn regex_match_dyn( | |
| /// Invoke a compute kernel on a data array and a scalar value with flag | ||
| macro_rules! regexp_is_match_flag_scalar { | ||
| ($LEFT:expr, $RIGHT:expr, $ARRAYTYPE:ident, $NOT:expr, $FLAG:expr) => {{ | ||
| let ll = $LEFT | ||
| .as_any() | ||
| .downcast_ref::<$ARRAYTYPE>() | ||
| .expect("failed to downcast array"); | ||
|
|
||
| if let Some(Some(string_value)) = $RIGHT.try_as_str() { | ||
| let flag = $FLAG.then_some("i"); | ||
| match regexp_is_match_scalar(ll, &string_value, flag) { | ||
| Ok(mut array) => { | ||
| if $NOT { | ||
| array = not(&array).unwrap(); | ||
| match $LEFT.as_any().downcast_ref::<$ARRAYTYPE>() { | ||
| None => internal_err!( | ||
| "failed to downcast array to {} for operation 'regex_match_dyn_scalar'", | ||
| stringify!($ARRAYTYPE) | ||
| ), | ||
| Some(ll) => { | ||
| if let Some(Some(string_value)) = $RIGHT.try_as_str() { | ||
| let flag = $FLAG.then_some("i"); | ||
| match regexp_is_match_scalar(ll, &string_value, flag) { | ||
| Ok(mut array) => { | ||
| if $NOT { | ||
| array = not(&array).unwrap(); | ||
| } | ||
| Ok(Arc::new(array)) | ||
| } | ||
| Err(e) => { | ||
| internal_err!("failed to call 'regex_match_dyn_scalar' {}", e) | ||
| } | ||
| } | ||
| Ok(Arc::new(array)) | ||
| } else { | ||
| internal_err!( | ||
| "failed to cast literal value {} for operation 'regex_match_dyn_scalar'", | ||
| $RIGHT | ||
| ) | ||
| } | ||
| Err(e) => internal_err!("failed to call 'regex_match_dyn_scalar' {}", e), | ||
| } | ||
| } else { | ||
| internal_err!( | ||
| "failed to cast literal value {} for operation 'regex_match_dyn_scalar'", | ||
| $RIGHT | ||
| ) | ||
| } | ||
| }}; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -927,7 +927,10 @@ impl<S: ContextProvider> SqlToRel<'_, S> { | |
| ) -> Result<Expr> { | ||
| let pattern = self.sql_expr_to_logical_expr(pattern, schema, planner_context)?; | ||
| let pattern_type = pattern.get_type(schema)?; | ||
| if pattern_type != DataType::Utf8 && pattern_type != DataType::Null { | ||
| if !matches!( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can use |
||
| pattern_type, | ||
| DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View | DataType::Null | ||
| ) { | ||
| return plan_err!("Invalid pattern in SIMILAR TO expression"); | ||
| } | ||
| let escape_char = match escape_char.map(|v| v.value) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.