3030import bigframes .operations .generic_ops as generic_ops
3131import bigframes .operations .numeric_ops as numeric_ops
3232import bigframes .operations .struct_ops as struct_ops
33+ import bigframes .operations .string_ops as string_ops
3334from bigframes .core import agg_expressions , bigframe_node , nodes , rewrite
3435from bigframes .core .compile import lowering
3536
@@ -43,9 +44,11 @@ def __init__(
4344 self ,
4445 duration_type : Literal ["interval_day" , "int" ],
4546 use_precision_types : bool = True ,
47+ dialect : Literal ["substrait-datafusion" , "substrait-acero" ] = "substrait-datafusion" ,
4648 ):
4749 self ._duration_type = duration_type
4850 self ._use_precision_types = use_precision_types
51+ self ._dialect = dialect
4952
5053 def compile (self , plan : bigframe_node .BigFrameNode ) -> Optional [bytes ]:
5154 """
@@ -56,7 +59,7 @@ def compile(self, plan: bigframe_node.BigFrameNode) -> Optional[bytes]:
5659
5760 # Need to bind types in before lowering
5861 plan = rewrite .bind_schema_to_tree (plan )
59- plan = lowering .lower_ops_to_substrait (plan )
62+ plan = lowering .lower_ops_to_substrait (plan , dialect = self . _dialect )
6063 pb_rel = self ._compile_node (plan )
6164
6265 pb_plan = plan_pb2 .Plan ()
@@ -73,10 +76,26 @@ def compile(self, plan: bigframe_node.BigFrameNode) -> Optional[bytes]:
7376 )
7477 )
7578
76- for name , anchor in self ._EXTENSIONS .items ():
79+ # Determine extensions dynamically based on execution engine
80+ extensions = dict (self ._EXTENSIONS )
81+ if self ._use_precision_types :
82+ # DataFusion supports standard "replace"
83+ extensions ["replace" ] = 76
84+ else :
85+ # Acero expects Arrow simple extension function URN for "replace_substring"
86+ extensions ["replace_substring" ] = 76
87+
88+ # Register Arrow simple extension URN at anchor 1
89+ arrow_uri = pb_plan .extension_urns .add ()
90+ arrow_uri .extension_urn_anchor = 1
91+ arrow_uri .urn = "urn:arrow:substrait_simple_extension_function"
92+
93+ for name , anchor in extensions .items ():
7794 ext = pb_plan .extensions .add ()
7895 ext .extension_function .function_anchor = anchor
7996 ext .extension_function .name = name
97+ if name == "replace_substring" and not self ._use_precision_types :
98+ ext .extension_function .extension_urn_reference = 1
8099
81100 return pb_plan .SerializeToString ()
82101
@@ -848,8 +867,64 @@ def _compile_astype(
848867 child : nodes .BigFrameNode ,
849868 ) -> algebra_pb2 .Expression :
850869 arg_expr = self ._compile_expression (inputs [0 ], child )
870+ from_dtype = self ._get_expression_dtype (inputs [0 ], child )
871+
872+ if op .to_type == dtypes .STRING_DTYPE :
873+ if from_dtype == dtypes .DATETIME_DTYPE :
874+ # This is only reached for Acero (dialect="substrait-acero"),
875+ # because DataFusion was lowered to ReplaceStrOp in lowering.py!
876+ if not self ._use_precision_types :
877+ # Cast to precision_timestamp with precision 0 first, then to string
878+ second_ts_expr = self ._compile_cast_with_type_dict (
879+ arg_expr , {"precision_timestamp" : {"precision" : 0 }}
880+ )
881+ return self ._compile_cast (second_ts_expr , dtypes .STRING_DTYPE )
882+
851883 return self ._compile_cast (arg_expr , op .to_type )
852884
885+ @_compile_op .register (string_ops .ReplaceStrOp )
886+ def _compile_replace_str (
887+ self ,
888+ op : string_ops .ReplaceStrOp ,
889+ inputs : Sequence [ex .Expression ],
890+ child : nodes .BigFrameNode ,
891+ ) -> algebra_pb2 .Expression :
892+ arg_expr = self ._compile_expression (inputs [0 ], child )
893+ return self ._compile_replace (arg_expr , op .pat , op .repl )
894+
895+ def _compile_cast_with_type_dict (
896+ self , input_expr : algebra_pb2 .Expression , type_dict : Dict [str , Any ]
897+ ) -> algebra_pb2 .Expression :
898+ pb_expr = algebra_pb2 .Expression ()
899+ cast = pb_expr .cast
900+ cast .input .CopyFrom (input_expr )
901+ json_format .ParseDict (type_dict , cast .type )
902+ cast .failure_behavior = (
903+ algebra_pb2 .Expression .Cast .FAILURE_BEHAVIOR_THROW_EXCEPTION
904+ )
905+ return pb_expr
906+
907+ def _compile_replace (
908+ self ,
909+ str_expr : algebra_pb2 .Expression ,
910+ search : str ,
911+ replacement : str ,
912+ ) -> algebra_pb2 .Expression :
913+ pb_expr = algebra_pb2 .Expression ()
914+ pb_expr .scalar_function .function_reference = 76 # "replace" or "replace_substring"
915+
916+ pb_expr .scalar_function .arguments .add ().value .CopyFrom (str_expr )
917+
918+ search_expr = algebra_pb2 .Expression ()
919+ search_expr .literal .string = search
920+ pb_expr .scalar_function .arguments .add ().value .CopyFrom (search_expr )
921+
922+ replace_expr = algebra_pb2 .Expression ()
923+ replace_expr .literal .string = replacement
924+ pb_expr .scalar_function .arguments .add ().value .CopyFrom (replace_expr )
925+
926+ return pb_expr
927+
853928 @_compile_op .register (struct_ops .StructOp )
854929 def _compile_struct_op (
855930 self ,
0 commit comments