1515from __future__ import annotations
1616
1717import abc
18- from typing import TYPE_CHECKING , Optional
18+ import asyncio
19+ from typing import TYPE_CHECKING , Optional , cast
1920
20- from bigframes .core import bigframe_node
21+ import bigframes .core .compile .substrait .compiler as substrait_compiler
22+ import bigframes .core .rewrite as rewrite
23+ from bigframes .core import bigframe_node , nodes
2124from bigframes .session import executor , semi_executor
22- import bigframes .core .rewrite .slices as slices_rewrite
23- from bigframes .core import nodes
24- import asyncio
2525
2626if TYPE_CHECKING :
2727 import pyarrow as pa
@@ -66,49 +66,93 @@ def consume(self, plan_proto: bytes, tables: dict[str, pa.Table]) -> pa.Table:
6666 ctx = datafusion .SessionContext ()
6767
6868 for name , table in tables .items ():
69- df = ctx .from_arrow_table (table )
70- ctx .register_table (name , df )
71-
69+ df = ctx .from_arrow_table (table )
70+ ctx .register_table (name , df )
71+
7272 import datafusion .substrait
7373
74- datafusion_substrait_plan = datafusion .substrait .Serde .deserialize_bytes (plan_proto )
75- logical_plan = datafusion .substrait .Consumer .from_substrait_plan (ctx , datafusion_substrait_plan )
74+ datafusion_substrait_plan = datafusion .substrait .Serde .deserialize_bytes (
75+ plan_proto
76+ )
77+ logical_plan = datafusion .substrait .Consumer .from_substrait_plan (
78+ ctx , datafusion_substrait_plan
79+ )
7680 df = ctx .create_dataframe_from_logical_plan (logical_plan )
7781 return df .to_arrow_table ()
7882
7983
84+ class AceroSubstraitConsumer (SubstraitConsumer ):
85+ """
86+ Executes Substrait plans using Apache Arrow Acero.
87+ """
88+
89+ def consume (self , plan_proto : bytes , tables : dict [str , pa .Table ]) -> pa .Table :
90+ import pyarrow .substrait as pa_substrait
91+
92+ def provide_table (name : list [str ], schema : pa .Schema ) -> pa .Table :
93+ return tables [name [0 ]]
94+
95+ batch_reader = pa_substrait .run_query (plan_proto , table_provider = provide_table )
96+ return batch_reader .read_all ()
97+
98+
8099class SubstraitExecutor (semi_executor .SemiExecutor ):
81100 """
82101 Executes plans by compiling them to Substrait and running them via a consumer.
83102 """
84103
85- def __init__ (self , consumer : SubstraitConsumer ):
104+ def __init__ (
105+ self ,
106+ consumer : SubstraitConsumer ,
107+ compiler : substrait_compiler .SubstraitCompiler ,
108+ ):
86109 self ._consumer = consumer
87- # Lazy import to avoid circular dependencies
88- from bigframes .core .compile .substrait .compiler import SubstraitCompiler
89- self ._compiler = SubstraitCompiler ()
110+ self ._compiler = compiler
111+
112+ @classmethod
113+ def default_for_engine (cls , engine_name : str ) -> SubstraitExecutor :
114+ if engine_name == "acero" :
115+ return cls (
116+ AceroSubstraitConsumer (),
117+ substrait_compiler .SubstraitCompiler (
118+ duration_type = "int" , use_precision_types = False
119+ ),
120+ )
121+ elif engine_name == "datafusion" :
122+ return cls (
123+ DataFusionSubstraitConsumer (),
124+ substrait_compiler .SubstraitCompiler (duration_type = "int" ),
125+ )
126+ else :
127+ raise ValueError (f"Unknown engine: { engine_name } " )
90128
91129 async def execute (
92130 self ,
93131 plan : bigframe_node .BigFrameNode ,
94132 ordered : bool ,
95133 peek : Optional [int ] = None ,
96134 ) -> Optional [executor .ExecuteResult ]:
97- plan = plan .bottom_up (slices_rewrite .rewrite_slice )
135+ plan = plan .bottom_up (rewrite .rewrite_slice )
136+ # Only needed for acero technically, datafusion can handle timedeltas
137+ plan = plan .bottom_up (rewrite .rewrite_timedelta_expressions )
138+
139+ from bigframes .core import expression
98140
99- from bigframes .core import expression , rewrite
100141 output_cols = tuple ((expression .DerefOp (id ), id .name ) for id in plan .ids )
101142 result_node = nodes .ResultNode (
102143 plan ,
103144 output_cols = output_cols ,
104145 )
105- import typing
106- result_node = typing .cast (nodes .ResultNode , rewrite .column_pruning (result_node ))
146+ result_node = cast (nodes .ResultNode , rewrite .column_pruning (result_node ))
107147 result_node = rewrite .defer_order (result_node , output_hidden_row_keys = False )
108148
109149 rewritten_plan = result_node .child
110150
111- if ordered and result_node .order_by and result_node .order_by .all_ordering_columns :
151+ if (
152+ ordered
153+ and result_node .order_by
154+ and result_node .order_by .all_ordering_columns
155+ ):
112156 rewritten_plan = nodes .OrderByNode (
113157 rewritten_plan ,
114158 by = tuple (result_node .order_by .all_ordering_columns ),
@@ -118,7 +162,9 @@ async def execute(
118162 if rewritten_plan .ids != original_ids :
119163 rewritten_plan = nodes .SelectionNode (
120164 rewritten_plan ,
121- input_output_pairs = tuple (nodes .AliasedRef .identity (id ) for id in original_ids )
165+ input_output_pairs = tuple (
166+ nodes .AliasedRef .identity (id ) for id in original_ids
167+ ),
122168 )
123169
124170 if not self ._can_execute (rewritten_plan ):
@@ -130,19 +176,24 @@ async def execute(
130176
131177 tables = {}
132178 for node in rewritten_plan .unique_nodes ():
133- if isinstance (node , nodes .ReadLocalNode ):
134- table_name = f"table_{ id (node )} "
135- table = node .local_data_source .data
136- table = table .select ([item .source_id for item in node .scan_list .items ])
137- table = table .rename_columns ([item .id .sql for item in node .scan_list .items ])
138- if node .offsets_col is not None :
139- from bigframes .core import pyarrow_utils
140- table = pyarrow_utils .append_offsets (table , node .offsets_col .sql )
141- tables [table_name ] = table
142-
143- pa_table = await asyncio .to_thread (self ._consumer .consume , substrait_plan_proto , tables )
144-
145- if peek is not None :
179+ if isinstance (node , nodes .ReadLocalNode ):
180+ table_name = f"table_{ id (node )} "
181+ table = node .local_data_source .to_pyarrow_table (duration_type = "int" )
182+ table = table .select ([item .source_id for item in node .scan_list .items ])
183+ table = table .rename_columns (
184+ [item .id .sql for item in node .scan_list .items ]
185+ )
186+ if node .offsets_col is not None :
187+ from bigframes .core import pyarrow_utils
188+
189+ table = pyarrow_utils .append_offsets (table , node .offsets_col .sql )
190+ tables [table_name ] = table
191+
192+ pa_table = await asyncio .to_thread (
193+ self ._consumer .consume , substrait_plan_proto , tables
194+ )
195+
196+ if peek is not None :
146197 pa_table = pa_table .slice (0 , peek )
147198
148199 return executor .LocalExecuteResult (
0 commit comments