@@ -43,7 +43,7 @@ use datafusion_expr::expr_rewriter::coerce_plan_expr_for_schema;
4343use datafusion_expr:: expr_schema:: cast_subquery;
4444use datafusion_expr:: logical_plan:: Subquery ;
4545use datafusion_expr:: type_coercion:: binary:: {
46- comparison_coercion, like_coercion, type_union_coercion,
46+ comparison_coercion, like_coercion, regex_coercion , type_union_coercion,
4747} ;
4848use datafusion_expr:: type_coercion:: functions:: {
4949 UDFCoercionExt , fields_with_udf, value_fields_with_higher_order_udf_and_lambdas,
@@ -442,6 +442,38 @@ impl<'a> TypeCoercionRewriter<'a> {
442442
443443 Ok ( e)
444444 }
445+
446+ /// Coerce the value and pattern expressions of a string pattern matching
447+ /// expression (`LIKE`, `ILIKE` or `SIMILAR TO`) to a common type using
448+ /// the provided coercion rules. `LIKE` can preserve a dictionary-encoded
449+ /// value expression, while regex array kernels require both operands to
450+ /// have the same physical string type.
451+ fn coerce_like_operands (
452+ & self ,
453+ expr : Expr ,
454+ pattern : Expr ,
455+ coercion : fn ( & DataType , & DataType ) -> Option < DataType > ,
456+ op_name : & str ,
457+ preserve_utf8_dictionary : bool ,
458+ ) -> Result < ( Box < Expr > , Box < Expr > ) > {
459+ let left_type = expr. get_type ( self . schema ) ?;
460+ let right_type = pattern. get_type ( self . schema ) ?;
461+ let coerced_type = coercion ( & left_type, & right_type) . ok_or_else ( || {
462+ plan_datafusion_err ! (
463+ "There isn't a common type to coerce {left_type} and {right_type} in {op_name} expression"
464+ )
465+ } ) ?;
466+ let expr = match left_type {
467+ DataType :: Dictionary ( _, inner)
468+ if preserve_utf8_dictionary && * inner == DataType :: Utf8 =>
469+ {
470+ Box :: new ( expr)
471+ }
472+ _ => Box :: new ( expr. cast_to ( & coerced_type, self . schema ) ?) ,
473+ } ;
474+ let pattern = Box :: new ( pattern. cast_to ( & coerced_type, self . schema ) ?) ;
475+ Ok ( ( expr, pattern) )
476+ }
445477}
446478
447479impl TreeNodeRewriter for TypeCoercionRewriter < ' _ > {
@@ -588,23 +620,14 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> {
588620 escape_char,
589621 case_insensitive,
590622 } ) => {
591- let left_type = expr. get_type ( self . schema ) ?;
592- let right_type = pattern. get_type ( self . schema ) ?;
593- let coerced_type = like_coercion ( & left_type, & right_type) . ok_or_else ( || {
594- let op_name = if case_insensitive {
595- "ILIKE"
596- } else {
597- "LIKE"
598- } ;
599- plan_datafusion_err ! (
600- "There isn't a common type to coerce {left_type} and {right_type} in {op_name} expression"
601- )
602- } ) ?;
603- let expr = match left_type {
604- DataType :: Dictionary ( _, inner) if * inner == DataType :: Utf8 => expr,
605- _ => Box :: new ( expr. cast_to ( & coerced_type, self . schema ) ?) ,
606- } ;
607- let pattern = Box :: new ( pattern. cast_to ( & coerced_type, self . schema ) ?) ;
623+ let op_name = if case_insensitive { "ILIKE" } else { "LIKE" } ;
624+ let ( expr, pattern) = self . coerce_like_operands (
625+ * expr,
626+ * pattern,
627+ like_coercion,
628+ op_name,
629+ true ,
630+ ) ?;
608631 Ok ( Transformed :: yes ( Expr :: Like ( Like :: new (
609632 negated,
610633 expr,
@@ -613,6 +636,32 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> {
613636 case_insensitive,
614637 ) ) ) )
615638 }
639+ Expr :: SimilarTo ( Like {
640+ negated,
641+ expr,
642+ pattern,
643+ escape_char,
644+ case_insensitive,
645+ } ) => {
646+ // `SIMILAR TO` is planned as a regex operator, so its operands
647+ // must be coerced to a common string type using the same
648+ // coercion rules as the physical regex operators. Otherwise
649+ // mismatched operand types panic during execution.
650+ let ( expr, pattern) = self . coerce_like_operands (
651+ * expr,
652+ * pattern,
653+ regex_coercion,
654+ "SIMILAR TO" ,
655+ false ,
656+ ) ?;
657+ Ok ( Transformed :: yes ( Expr :: SimilarTo ( Like :: new (
658+ negated,
659+ expr,
660+ pattern,
661+ escape_char,
662+ case_insensitive,
663+ ) ) ) )
664+ }
616665 Expr :: BinaryExpr ( BinaryExpr { left, op, right } ) => {
617666 let ( left, right) =
618667 self . coerce_binary_op ( * left, self . schema , op, * right, self . schema ) ?;
@@ -812,7 +861,6 @@ impl TreeNodeRewriter for TypeCoercionRewriter<'_> {
812861 | Expr :: Column ( _)
813862 | Expr :: ScalarVariable ( _, _)
814863 | Expr :: Literal ( _, _)
815- | Expr :: SimilarTo ( _)
816864 | Expr :: IsNotNull ( _)
817865 | Expr :: IsNull ( _)
818866 | Expr :: Cast ( _)
@@ -2240,6 +2288,113 @@ mod test {
22402288 Ok ( ( ) )
22412289 }
22422290
2291+ #[ test]
2292+ fn similar_to_for_type_coercion ( ) -> Result < ( ) > {
2293+ // similar to : utf8 similar to "abc"
2294+ let expr = Box :: new ( col ( "a" ) ) ;
2295+ let pattern = Box :: new ( lit ( ScalarValue :: new_utf8 ( "abc" ) ) ) ;
2296+ let similar_to_expr =
2297+ Expr :: SimilarTo ( Like :: new ( false , expr, pattern, None , false ) ) ;
2298+ let empty = empty_with_type ( Utf8 ) ;
2299+ let plan =
2300+ LogicalPlan :: Projection ( Projection :: try_new ( vec ! [ similar_to_expr] , empty) ?) ;
2301+
2302+ assert_analyzed_plan_eq ! (
2303+ plan,
2304+ @r#"
2305+ Projection: a SIMILAR TO Utf8("abc")
2306+ EmptyRelation: rows=0
2307+ "#
2308+ ) ?;
2309+
2310+ // NULL pattern is coerced to a typed NULL instead of panicking
2311+ // (https://github.com/apache/datafusion/issues/22886)
2312+ let expr = Box :: new ( col ( "a" ) ) ;
2313+ let pattern = Box :: new ( lit ( ScalarValue :: Null ) ) ;
2314+ let similar_to_expr =
2315+ Expr :: SimilarTo ( Like :: new ( false , expr, pattern, None , false ) ) ;
2316+ let empty = empty_with_type ( Utf8 ) ;
2317+ let plan =
2318+ LogicalPlan :: Projection ( Projection :: try_new ( vec ! [ similar_to_expr] , empty) ?) ;
2319+
2320+ assert_analyzed_plan_eq ! (
2321+ plan,
2322+ @r"
2323+ Projection: a SIMILAR TO CAST(NULL AS Utf8)
2324+ EmptyRelation: rows=0
2325+ "
2326+ ) ?;
2327+
2328+ // Utf8View value and Utf8 pattern are coerced to Utf8View
2329+ let expr = Box :: new ( col ( "a" ) ) ;
2330+ let pattern = Box :: new ( lit ( ScalarValue :: new_utf8 ( "abc" ) ) ) ;
2331+ let similar_to_expr =
2332+ Expr :: SimilarTo ( Like :: new ( false , expr, pattern, None , false ) ) ;
2333+ let empty = empty_with_type ( DataType :: Utf8View ) ;
2334+ let plan =
2335+ LogicalPlan :: Projection ( Projection :: try_new ( vec ! [ similar_to_expr] , empty) ?) ;
2336+
2337+ assert_analyzed_plan_eq ! (
2338+ plan,
2339+ @r#"
2340+ Projection: a SIMILAR TO CAST(Utf8("abc") AS Utf8View)
2341+ EmptyRelation: rows=0
2342+ "#
2343+ ) ?;
2344+
2345+ // Utf8 value and Utf8View pattern are coerced to Utf8View
2346+ let expr = Box :: new ( col ( "a" ) ) ;
2347+ let pattern = Box :: new ( lit ( ScalarValue :: Utf8View ( Some ( "abc" . to_string ( ) ) ) ) ) ;
2348+ let similar_to_expr =
2349+ Expr :: SimilarTo ( Like :: new ( false , expr, pattern, None , false ) ) ;
2350+ let empty = empty_with_type ( Utf8 ) ;
2351+ let plan =
2352+ LogicalPlan :: Projection ( Projection :: try_new ( vec ! [ similar_to_expr] , empty) ?) ;
2353+
2354+ assert_analyzed_plan_eq ! (
2355+ plan,
2356+ @r#"
2357+ Projection: CAST(a AS Utf8View) SIMILAR TO Utf8View("abc")
2358+ EmptyRelation: rows=0
2359+ "#
2360+ ) ?;
2361+
2362+ // Dictionary values are coerced to the common regex operand type
2363+ let expr = Box :: new ( col ( "a" ) ) ;
2364+ let pattern = Box :: new ( lit ( ScalarValue :: new_utf8 ( "abc" ) ) ) ;
2365+ let similar_to_expr =
2366+ Expr :: SimilarTo ( Like :: new ( false , expr, pattern, None , false ) ) ;
2367+ let empty = empty_with_type ( DataType :: Dictionary (
2368+ Box :: new ( DataType :: Int32 ) ,
2369+ Box :: new ( Utf8 ) ,
2370+ ) ) ;
2371+ let plan =
2372+ LogicalPlan :: Projection ( Projection :: try_new ( vec ! [ similar_to_expr] , empty) ?) ;
2373+
2374+ assert_analyzed_plan_eq ! (
2375+ plan,
2376+ @r#"
2377+ Projection: CAST(a AS Utf8) SIMILAR TO Utf8("abc")
2378+ EmptyRelation: rows=0
2379+ "#
2380+ ) ?;
2381+
2382+ // incompatible types are a planning error, not a panic
2383+ let expr = Box :: new ( col ( "a" ) ) ;
2384+ let pattern = Box :: new ( lit ( ScalarValue :: new_utf8 ( "abc" ) ) ) ;
2385+ let similar_to_expr =
2386+ Expr :: SimilarTo ( Like :: new ( false , expr, pattern, None , false ) ) ;
2387+ let empty = empty_with_type ( DataType :: Int64 ) ;
2388+ let plan =
2389+ LogicalPlan :: Projection ( Projection :: try_new ( vec ! [ similar_to_expr] , empty) ?) ;
2390+ assert_type_coercion_error (
2391+ plan,
2392+ "There isn't a common type to coerce Int64 and Utf8 in SIMILAR TO expression" ,
2393+ ) ?;
2394+
2395+ Ok ( ( ) )
2396+ }
2397+
22432398 #[ test]
22442399 fn unknown_for_type_coercion ( ) -> Result < ( ) > {
22452400 // unknown
0 commit comments