@@ -346,3 +346,192 @@ mod test {
346346 Ok ( ( ) )
347347 }
348348}
349+
350+ /// Tests for the `try_to_proto` / `try_from_proto` hooks (issue #22418).
351+ ///
352+ /// These drive the encode/decode hooks directly with small stub
353+ /// encoder/decoders so every branch can be covered without depending on
354+ /// `datafusion-proto`: the happy path, the wrong-node and missing-child
355+ /// rejections, and child encode/decode error propagation.
356+ #[ cfg( all( test, feature = "proto" ) ) ]
357+ mod proto_tests {
358+ use super :: * ;
359+ use crate :: expressions:: { Column , col} ;
360+ use crate :: proto_test_util:: {
361+ StubDecoder , StubEncoder , UnreachableDecoder , column_node,
362+ } ;
363+ use arrow:: datatypes:: Field ;
364+ use datafusion_common:: DataFusionError ;
365+ use datafusion_physical_expr_common:: physical_expr:: proto_decode:: PhysicalExprDecodeCtx ;
366+ use datafusion_physical_expr_common:: physical_expr:: proto_encode:: PhysicalExprEncodeCtx ;
367+ use datafusion_proto_models:: protobuf:: {
368+ PhysicalExprNode , PhysicalLikeExprNode , physical_expr_node,
369+ } ;
370+
371+ /// Build a `LikeExpr` proto node with the given children.
372+ fn like_node (
373+ negated : bool ,
374+ case_insensitive : bool ,
375+ expr : Option < Box < PhysicalExprNode > > ,
376+ pattern : Option < Box < PhysicalExprNode > > ,
377+ ) -> PhysicalExprNode {
378+ PhysicalExprNode {
379+ expr_id : None ,
380+ expr_type : Some ( physical_expr_node:: ExprType :: LikeExpr ( Box :: new (
381+ PhysicalLikeExprNode {
382+ negated,
383+ case_insensitive,
384+ expr,
385+ pattern,
386+ } ,
387+ ) ) ) ,
388+ }
389+ }
390+
391+ /// A `LikeExpr` over two `Utf8` columns with both flags set, so the
392+ /// `negated` / `case_insensitive` wiring is actually exercised.
393+ fn like_fixture ( ) -> LikeExpr {
394+ let schema = Schema :: new ( vec ! [
395+ Field :: new( "a" , DataType :: Utf8 , false ) ,
396+ Field :: new( "b" , DataType :: Utf8 , false ) ,
397+ ] ) ;
398+ LikeExpr :: new (
399+ true ,
400+ true ,
401+ col ( "a" , & schema) . unwrap ( ) ,
402+ col ( "b" , & schema) . unwrap ( ) ,
403+ )
404+ }
405+
406+ #[ test]
407+ fn try_to_proto_encodes_like_expr ( ) {
408+ let like = like_fixture ( ) ;
409+ let encoder = StubEncoder :: ok ( ) ;
410+ let ctx = PhysicalExprEncodeCtx :: new ( & encoder) ;
411+
412+ let node = like
413+ . try_to_proto ( & ctx)
414+ . unwrap ( )
415+ . expect ( "LikeExpr should encode to Some(node)" ) ;
416+
417+ // Built-in exprs never set expr_id; only dynamic filters do.
418+ assert ! ( node. expr_id. is_none( ) ) ;
419+ let like_node = match node. expr_type {
420+ Some ( physical_expr_node:: ExprType :: LikeExpr ( boxed) ) => * boxed,
421+ other => panic ! ( "expected a LikeExpr node, got {other:?}" ) ,
422+ } ;
423+ assert ! ( like_node. negated) ;
424+ assert ! ( like_node. case_insensitive) ;
425+ assert ! ( like_node. expr. is_some( ) ) ;
426+ assert ! ( like_node. pattern. is_some( ) ) ;
427+ }
428+
429+ #[ test]
430+ fn try_to_proto_propagates_expr_encode_error ( ) {
431+ let like = like_fixture ( ) ;
432+ let encoder = StubEncoder :: failing_on ( 1 ) ;
433+ let ctx = PhysicalExprEncodeCtx :: new ( & encoder) ;
434+ let err = like. try_to_proto ( & ctx) . unwrap_err ( ) ;
435+ assert ! ( matches!( err, DataFusionError :: Internal ( msg) if msg. contains( "call 1" ) ) ) ;
436+ }
437+
438+ #[ test]
439+ fn try_to_proto_propagates_pattern_encode_error ( ) {
440+ let like = like_fixture ( ) ;
441+ let encoder = StubEncoder :: failing_on ( 2 ) ;
442+ let ctx = PhysicalExprEncodeCtx :: new ( & encoder) ;
443+ let err = like. try_to_proto ( & ctx) . unwrap_err ( ) ;
444+ assert ! ( matches!( err, DataFusionError :: Internal ( msg) if msg. contains( "call 2" ) ) ) ;
445+ }
446+
447+ #[ test]
448+ fn try_from_proto_decodes_like_expr ( ) {
449+ let node = like_node (
450+ true ,
451+ true ,
452+ Some ( Box :: new ( column_node ( "a" ) ) ) ,
453+ Some ( Box :: new ( column_node ( "b" ) ) ) ,
454+ ) ;
455+ let schema = Schema :: empty ( ) ;
456+ let decoder = StubDecoder :: ok ( ) ;
457+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
458+
459+ let decoded = LikeExpr :: try_from_proto ( & node, & ctx) . unwrap ( ) ;
460+ let like = decoded
461+ . downcast_ref :: < LikeExpr > ( )
462+ . expect ( "decoded expr should be a LikeExpr" ) ;
463+ assert ! ( like. negated( ) ) ;
464+ assert ! ( like. case_insensitive( ) ) ;
465+ assert ! ( like. expr( ) . downcast_ref:: <Column >( ) . is_some( ) ) ;
466+ assert ! ( like. pattern( ) . downcast_ref:: <Column >( ) . is_some( ) ) ;
467+ }
468+
469+ #[ test]
470+ fn try_from_proto_rejects_non_like_node ( ) {
471+ let node = column_node ( "a" ) ;
472+ let schema = Schema :: empty ( ) ;
473+ let decoder = UnreachableDecoder ;
474+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
475+ let err = LikeExpr :: try_from_proto ( & node, & ctx) . unwrap_err ( ) ;
476+ assert ! ( matches!(
477+ err,
478+ DataFusionError :: Internal ( msg) if msg == "PhysicalExprNode is not a LikeExpr"
479+ ) ) ;
480+ }
481+
482+ #[ test]
483+ fn try_from_proto_rejects_missing_expr ( ) {
484+ let node = like_node ( false , false , None , Some ( Box :: new ( column_node ( "b" ) ) ) ) ;
485+ let schema = Schema :: empty ( ) ;
486+ let decoder = UnreachableDecoder ;
487+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
488+ let err = LikeExpr :: try_from_proto ( & node, & ctx) . unwrap_err ( ) ;
489+ assert ! ( matches!(
490+ err,
491+ DataFusionError :: Internal ( msg) if msg == "LikeExpr is missing required field 'expr'"
492+ ) ) ;
493+ }
494+
495+ #[ test]
496+ fn try_from_proto_rejects_missing_pattern ( ) {
497+ let node = like_node ( false , false , Some ( Box :: new ( column_node ( "a" ) ) ) , None ) ;
498+ let schema = Schema :: empty ( ) ;
499+ let decoder = UnreachableDecoder ;
500+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
501+ let err = LikeExpr :: try_from_proto ( & node, & ctx) . unwrap_err ( ) ;
502+ assert ! ( matches!(
503+ err,
504+ DataFusionError :: Internal ( msg) if msg == "LikeExpr is missing required field 'pattern'"
505+ ) ) ;
506+ }
507+
508+ #[ test]
509+ fn try_from_proto_propagates_expr_decode_error ( ) {
510+ let node = like_node (
511+ false ,
512+ false ,
513+ Some ( Box :: new ( column_node ( "a" ) ) ) ,
514+ Some ( Box :: new ( column_node ( "b" ) ) ) ,
515+ ) ;
516+ let schema = Schema :: empty ( ) ;
517+ let decoder = StubDecoder :: failing_on ( 1 ) ;
518+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
519+ let err = LikeExpr :: try_from_proto ( & node, & ctx) . unwrap_err ( ) ;
520+ assert ! ( matches!( err, DataFusionError :: Internal ( msg) if msg. contains( "call 1" ) ) ) ;
521+ }
522+
523+ #[ test]
524+ fn try_from_proto_propagates_pattern_decode_error ( ) {
525+ let node = like_node (
526+ false ,
527+ false ,
528+ Some ( Box :: new ( column_node ( "a" ) ) ) ,
529+ Some ( Box :: new ( column_node ( "b" ) ) ) ,
530+ ) ;
531+ let schema = Schema :: empty ( ) ;
532+ let decoder = StubDecoder :: failing_on ( 2 ) ;
533+ let ctx = PhysicalExprDecodeCtx :: new ( & schema, & decoder) ;
534+ let err = LikeExpr :: try_from_proto ( & node, & ctx) . unwrap_err ( ) ;
535+ assert ! ( matches!( err, DataFusionError :: Internal ( msg) if msg. contains( "call 2" ) ) ) ;
536+ }
537+ }
0 commit comments