Skip to content

Commit 0fd0b60

Browse files
add more ops to substrait compiler
1 parent 4b2129e commit 0fd0b60

2 files changed

Lines changed: 213 additions & 26 deletions

File tree

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

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,15 @@ def compile(self, plan: bigframe_node.BigFrameNode) -> Optional[bytes]:
5353
("sub", 2),
5454
("mul", 3),
5555
("div", 4),
56-
("eq", 5),
56+
("equal", 5),
5757
("ne", 6),
5858
("lt", 7),
5959
("gt", 8),
6060
("le", 9),
6161
("ge", 10),
62+
("sum", 11),
63+
("max", 12),
64+
("and", 13),
6265
]
6366
for name, anchor in extensions:
6467
ext = pb_plan.extensions.add()
@@ -90,8 +93,6 @@ def _compile_node(self, node: bigframe_node.BigFrameNode) -> Dict[str, Any]:
9093
return self._compile_selection(node)
9194
elif isinstance(node, nodes.FilterNode):
9295
return self._compile_filter(node)
93-
elif isinstance(node, nodes.SliceNode):
94-
return self._compile_slice(node)
9596
elif isinstance(node, nodes.ProjectionNode):
9697
return self._compile_projection(node)
9798
elif isinstance(node, nodes.JoinNode):
@@ -114,43 +115,35 @@ def _compile_read(self, node: nodes.ReadLocalNode) -> Dict[str, Any]:
114115
return json_format.MessageToDict(rel, preserving_proto_field_name=True)
115116

116117
def _compile_selection(self, node: nodes.SelectionNode) -> Dict[str, Any]:
117-
# Selection usually maps to ProjectRel or FilterRel depending on if it filters or just selects columns.
118-
# If it's just column selection (Projection), it's a ProjectRel.
119-
# Let's assume it's a ProjectRel for now.
120118
input_rel = self._compile_node(node.child)
119+
expressions = []
120+
child_ids = list(node.child.ids)
121+
for aliased_ref in node.input_output_pairs:
122+
source_id = aliased_ref.ref.id
123+
idx = child_ids.index(source_id)
124+
expressions.append({"selection": {"direct_reference": {"struct_field": {"field": idx}}}})
121125
return {
122126
"project": {
127+
"common": {
128+
"emit": {
129+
"outputMapping": [len(child_ids) + i for i in range(len(expressions))]
130+
}
131+
},
123132
"input": input_rel,
124-
"expressions": [
125-
# Skeletal expression mapping
126-
{"selection": {"direct_reference": {"struct_field": {"field": i}}}}
127-
for i in range(len(node.schema))
128-
]
133+
"expressions": expressions
129134
}
130135
}
131136

132137
def _compile_filter(self, node: nodes.FilterNode) -> Dict[str, Any]:
133138
input_rel = self._compile_node(node.child)
134-
condition_rel = self._compile_expression(node.condition, node.child)
139+
condition_rel = self._compile_expression(node.predicate, node.child)
135140
return {
136141
"filter": {
137142
"input": input_rel,
138143
"condition": condition_rel
139144
}
140145
}
141146

142-
def _compile_slice(self, node: nodes.SliceNode) -> Dict[str, Any]:
143-
input_rel = self._compile_node(node.child)
144-
count = node.stop if node.stop is not None else -1
145-
offset = node.start if node.start is not None else 0
146-
147-
return {
148-
"fetch": {
149-
"input": input_rel,
150-
"offset": offset,
151-
"count": count
152-
}
153-
}
154147

155148
def _compile_projection(self, node: nodes.ProjectionNode) -> Dict[str, Any]:
156149
input_rel_dict = self._compile_node(node.child)
@@ -193,7 +186,7 @@ def _compile_join(self, node: nodes.JoinNode) -> Dict[str, Any]:
193186

194187
eq_expressions.append({
195188
"scalar_function": {
196-
"function_reference": 0,
189+
"function_reference": 5, # eq
197190
"arguments": [
198191
{"value": {"selection": {"direct_reference": {"struct_field": {"field": left_idx}}}}},
199192
{"value": {"selection": {"direct_reference": {"struct_field": {"field": right_idx}}}}}
@@ -203,6 +196,13 @@ def _compile_join(self, node: nodes.JoinNode) -> Dict[str, Any]:
203196

204197
if len(eq_expressions) > 1:
205198
expr = eq_expressions[0]
199+
for e in eq_expressions[1:]:
200+
expr = {
201+
"scalar_function": {
202+
"function_reference": 13, # and
203+
"arguments": [{"value": expr}, {"value": e}]
204+
}
205+
}
206206
elif len(eq_expressions) == 1:
207207
expr = eq_expressions[0]
208208
else:
@@ -229,8 +229,14 @@ def _compile_aggregate(self, node: nodes.AggregateNode) -> Dict[str, Any]:
229229
groupings.append({"grouping_expressions": grouping_expressions})
230230

231231
measures = []
232+
import bigframes.operations.aggregations as agg_ops
232233
for agg, _ in node.aggregations:
233-
func_ref = 1 if "Sum" in type(agg).__name__ else 2
234+
if isinstance(agg.op, agg_ops.SumOp):
235+
func_ref = 11
236+
elif isinstance(agg.op, agg_ops.MaxOp):
237+
func_ref = 12
238+
else:
239+
raise NotImplementedError(f"Aggregation {type(agg.op)} not supported in Substrait compiler yet")
234240
args = []
235241
if hasattr(agg, "column_references"):
236242
for col_id in agg.column_references:

packages/bigframes/tests/unit/session/test_substrait_executor.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,184 @@ def test_execute_projection_add_with_datafusion():
133133
assert "b" in result_table.column_names
134134
assert "a" in result_table.column_names
135135
assert result_table.column("b").to_pylist() == [43, 44, 45]
136+
137+
138+
def test_execute_filter_with_datafusion():
139+
from bigframes.session.substrait_executor import DataFusionSubstraitConsumer
140+
from bigframes.operations.comparison_ops import gt_op
141+
142+
consumer = DataFusionSubstraitConsumer()
143+
executor = substrait_executor.SubstraitExecutor(consumer)
144+
145+
read_node = create_read_local_node()
146+
147+
# a > 1
148+
filter_expr = ex.OpExpression(
149+
op=gt_op,
150+
inputs=(
151+
ex.DerefOp(identifiers.ColumnId("a")),
152+
ex.ScalarConstantExpression(1),
153+
),
154+
)
155+
plan = nodes.FilterNode(
156+
child=read_node,
157+
predicate=filter_expr,
158+
)
159+
160+
result = executor.execute(plan, ordered=True)
161+
assert result is not None
162+
163+
result_table = pa.Table.from_batches(result.batches().arrow_batches)
164+
assert result_table.num_rows == 2
165+
assert "a" in result_table.column_names
166+
assert result_table.column("a").to_pylist() == [2, 3]
167+
168+
169+
def test_execute_aggregate_sum_with_datafusion():
170+
from bigframes.session.substrait_executor import DataFusionSubstraitConsumer
171+
from bigframes.operations.aggregations import sum_op
172+
from bigframes.core.agg_expressions import UnaryAggregation
173+
174+
consumer = DataFusionSubstraitConsumer()
175+
executor = substrait_executor.SubstraitExecutor(consumer)
176+
177+
read_node = create_read_local_node()
178+
179+
# sum(a)
180+
sum_agg = UnaryAggregation(
181+
op=sum_op,
182+
arg=ex.DerefOp(identifiers.ColumnId("a")),
183+
)
184+
185+
plan = nodes.AggregateNode(
186+
child=read_node,
187+
aggregations=((sum_agg, identifiers.ColumnId("sum_a")),),
188+
by_column_ids=(),
189+
)
190+
191+
result = executor.execute(plan, ordered=True)
192+
assert result is not None
193+
194+
result_table = pa.Table.from_batches(result.batches().arrow_batches)
195+
assert result_table.num_rows == 1
196+
assert "sum_a" in result_table.column_names
197+
assert result_table.column("sum_a").to_pylist() == [6]
198+
199+
200+
def test_execute_aggregate_max_with_datafusion():
201+
from bigframes.session.substrait_executor import DataFusionSubstraitConsumer
202+
from bigframes.operations.aggregations import max_op
203+
from bigframes.core.agg_expressions import UnaryAggregation
204+
205+
consumer = DataFusionSubstraitConsumer()
206+
executor = substrait_executor.SubstraitExecutor(consumer)
207+
208+
read_node = create_read_local_node()
209+
210+
# max(a)
211+
max_agg = UnaryAggregation(
212+
op=max_op,
213+
arg=ex.DerefOp(identifiers.ColumnId("a")),
214+
)
215+
216+
plan = nodes.AggregateNode(
217+
child=read_node,
218+
aggregations=((max_agg, identifiers.ColumnId("max_a")),),
219+
by_column_ids=(),
220+
)
221+
222+
result = executor.execute(plan, ordered=True)
223+
assert result is not None
224+
225+
result_table = pa.Table.from_batches(result.batches().arrow_batches)
226+
assert result_table.num_rows == 1
227+
assert "max_a" in result_table.column_names
228+
assert result_table.column("max_a").to_pylist() == [3]
229+
230+
231+
def test_execute_join_with_datafusion():
232+
from bigframes.session.substrait_executor import DataFusionSubstraitConsumer
233+
234+
consumer = DataFusionSubstraitConsumer()
235+
executor = substrait_executor.SubstraitExecutor(consumer)
236+
237+
# Table 1: a
238+
session1 = mocks.create_bigquery_session()
239+
table1 = pa.Table.from_pydict({"a": [1, 2, 3]})
240+
source1 = local_data.ManagedArrowTable.from_pyarrow(table1)
241+
col_id_a = identifiers.ColumnId("a")
242+
read_node1 = nodes.ReadLocalNode(
243+
local_data_source=source1,
244+
session=session1,
245+
scan_list=nodes.ScanList(items=(nodes.ScanItem(id=col_id_a, source_id="a"),)),
246+
)
247+
248+
# Table 2: b
249+
session2 = mocks.create_bigquery_session()
250+
table2 = pa.Table.from_pydict({"b": [2, 3, 4]})
251+
source2 = local_data.ManagedArrowTable.from_pyarrow(table2)
252+
col_id_b = identifiers.ColumnId("b")
253+
read_node2 = nodes.ReadLocalNode(
254+
local_data_source=source2,
255+
session=session2,
256+
scan_list=nodes.ScanList(items=(nodes.ScanItem(id=col_id_b, source_id="b"),)),
257+
)
258+
259+
# Join on a = b
260+
join_node = nodes.JoinNode(
261+
left_child=read_node1,
262+
right_child=read_node2,
263+
conditions=((ex.DerefOp(col_id_a), ex.DerefOp(col_id_b)),),
264+
type="inner",
265+
propogate_order=False,
266+
)
267+
268+
result = executor.execute(join_node, ordered=True)
269+
assert result is not None
270+
271+
result_table = pa.Table.from_batches(result.batches().arrow_batches)
272+
assert result_table.num_rows == 2
273+
assert "a" in result_table.column_names
274+
assert "b" in result_table.column_names
275+
assert result_table.column("a").to_pylist() == [2, 3]
276+
assert result_table.column("b").to_pylist() == [2, 3]
277+
278+
279+
def test_execute_selection_with_datafusion():
280+
from bigframes.session.substrait_executor import DataFusionSubstraitConsumer
281+
from bigframes.core.nodes import AliasedRef
282+
283+
consumer = DataFusionSubstraitConsumer()
284+
executor = substrait_executor.SubstraitExecutor(consumer)
285+
286+
# Table with a and b
287+
session = mocks.create_bigquery_session()
288+
table = pa.Table.from_pydict({"a": [1, 2, 3], "b": [4, 5, 6]})
289+
source = local_data.ManagedArrowTable.from_pyarrow(table)
290+
col_id_a = identifiers.ColumnId("a")
291+
col_id_b = identifiers.ColumnId("b")
292+
read_node = nodes.ReadLocalNode(
293+
local_data_source=source,
294+
session=session,
295+
scan_list=nodes.ScanList(
296+
items=(
297+
nodes.ScanItem(id=col_id_a, source_id="a"),
298+
nodes.ScanItem(id=col_id_b, source_id="b"),
299+
)
300+
),
301+
)
302+
303+
# Select only a, and rename it to c
304+
col_id_c = identifiers.ColumnId("c")
305+
selection_node = nodes.SelectionNode(
306+
child=read_node,
307+
input_output_pairs=(AliasedRef(ex.DerefOp(col_id_a), col_id_c),),
308+
)
309+
310+
result = executor.execute(selection_node, ordered=True)
311+
assert result is not None
312+
313+
result_table = pa.Table.from_batches(result.batches().arrow_batches)
314+
assert result_table.num_rows == 3
315+
assert result_table.column_names == ["c"]
316+
assert result_table.column("c").to_pylist() == [1, 2, 3]

0 commit comments

Comments
 (0)