Skip to content

Commit cf38b04

Browse files
simplify, add acero engine
1 parent 51761d7 commit cf38b04

8 files changed

Lines changed: 952 additions & 1585 deletions

File tree

packages/bigframes/bigframes/core/compile/substrait/compiler.py

Lines changed: 798 additions & 1145 deletions
Large diffs are not rendered by default.

packages/bigframes/bigframes/core/compile/substrait/test_multiindex_drop.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

packages/bigframes/bigframes/session/substrait_executor.py

Lines changed: 84 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
from __future__ import annotations
1616

1717
import 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
2124
from 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

2626
if 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+
8099
class 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(

packages/bigframes/bigframes/testing/substrait_session.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from __future__ import annotations
1616

17-
import dataclasses
17+
import asyncio
1818
import weakref
1919
from typing import TYPE_CHECKING, Union
2020

@@ -31,11 +31,13 @@
3131
import bigframes.core
3232

3333

34-
@dataclasses.dataclass
3534
class SubstraitTestExecutor(bigframes.session.executor.Executor):
36-
def __init__(self):
37-
from bigframes.session.substrait_executor import DataFusionSubstraitConsumer, SubstraitExecutor
38-
self.executor = SubstraitExecutor(DataFusionSubstraitConsumer())
35+
def __init__(
36+
self, consumer: bigframes.session.substrait_executor.SubstraitConsumer
37+
):
38+
from bigframes.session.substrait_executor import SubstraitExecutor
39+
40+
self.executor = SubstraitExecutor(consumer)
3941

4042
def execute(
4143
self,
@@ -46,11 +48,15 @@ def execute(
4648
raise ValueError(
4749
f"SubstraitTestExecutor does not support destination spec: {execution_spec.destination_spec}"
4850
)
49-
50-
result = self.executor.execute(array_value.node, ordered=True, peek=execution_spec.peek)
51+
52+
result = asyncio.run(
53+
self.executor.execute(
54+
array_value.node, ordered=True, peek=execution_spec.peek
55+
)
56+
)
5157
if result is None:
5258
raise NotImplementedError("SubstraitExecutor cannot execute this plan")
53-
59+
5460
return result
5561

5662
def cached(
@@ -63,7 +69,7 @@ def cached(
6369

6470

6571
class TestSession(bigframes.session.Session):
66-
def __init__(self):
72+
def __init__(self, executor: SubstraitTestExecutor):
6773
self._location = None # type: ignore
6874
self._bq_kms_key_name = None # type: ignore
6975
self._clients_provider = None # type: ignore
@@ -85,7 +91,7 @@ def __init__(self):
8591
self._metrics = bigframes.session.metrics.ExecutionMetrics()
8692
self._function_session = None # type: ignore
8793
self._temp_storage_manager = None # type: ignore
88-
self._executor = SubstraitTestExecutor()
94+
self._executor = executor
8995
self._loader = None # type: ignore
9096

9197
def read_pandas(self, pandas_dataframe, write_engine="default"):

packages/bigframes/tests/system/small/engines/conftest.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,34 @@ def sqlglot_engine(
8282

8383

8484
@pytest.fixture(scope="session")
85-
def substrait_datafusion_engine(
86-
) -> semi_executor.SemiExecutor:
87-
return substrait_executor.SubstraitExecutor(
88-
consumer = substrait_executor.DataFusionSubstraitConsumer()
89-
)
85+
def substrait_datafusion_engine() -> semi_executor.SemiExecutor:
86+
return substrait_executor.SubstraitExecutor.default_for_engine("datafusion")
9087

9188

92-
@pytest.fixture(scope="session", params=["pyarrow", "polars", "bq", "bq-sqlglot", "substrait-datafusion"])
89+
@pytest.fixture(scope="session")
90+
def substrait_acero_engine() -> semi_executor.SemiExecutor:
91+
return substrait_executor.SubstraitExecutor.default_for_engine("acero")
92+
93+
94+
@pytest.fixture(
95+
scope="session",
96+
params=[
97+
"pyarrow",
98+
"polars",
99+
"bq",
100+
"bq-sqlglot",
101+
"substrait-datafusion",
102+
"substrait-acero",
103+
],
104+
)
93105
def engine(
94-
request, pyarrow_engine, polars_engine, bq_engine, sqlglot_engine, substrait_datafusion_engine
106+
request,
107+
pyarrow_engine,
108+
polars_engine,
109+
bq_engine,
110+
sqlglot_engine,
111+
substrait_datafusion_engine,
112+
substrait_acero_engine,
95113
) -> semi_executor.SemiExecutor:
96114
if request.param == "pyarrow":
97115
return pyarrow_engine
@@ -103,6 +121,8 @@ def engine(
103121
return sqlglot_engine
104122
if request.param == "substrait-datafusion":
105123
return substrait_datafusion_engine
124+
if request.param == "substrait-acero":
125+
return substrait_acero_engine
106126
raise ValueError(f"Unrecognized param: {request.param}")
107127

108128

packages/bigframes/tests/system/small/engines/test_aggregation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def apply_agg_to_all_valid(
5454
return new_arr
5555

5656

57-
@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot", "substrait-datafusion"], indirect=True)
57+
@pytest.mark.parametrize(
58+
"engine", ["polars", "bq", "bq-sqlglot", "substrait-datafusion"], indirect=True
59+
)
5860
def test_engines_aggregate_post_filter_size(
5961
scalars_array_value: array_value.ArrayValue,
6062
engine,

0 commit comments

Comments
 (0)