Skip to content

Commit 28cd7b9

Browse files
add agg/window support
1 parent df454d0 commit 28cd7b9

6 files changed

Lines changed: 528 additions & 38 deletions

File tree

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

Lines changed: 207 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ def compile(self, plan: bigframe_node.BigFrameNode) -> Optional[bytes]:
6767

6868
for item in plan.schema.items:
6969
plan_rel.root.names.extend(
70-
self._get_substrait_names(item.column, item.dtype)
70+
self._get_substrait_names(
71+
item.column if isinstance(item.column, str) else item.column.sql,
72+
item.dtype,
73+
)
7174
)
7275

7376
for name, anchor in self._EXTENSIONS.items():
@@ -88,6 +91,7 @@ def can_compile(self, plan: bigframe_node.BigFrameNode) -> bool:
8891
nodes.ProjectionNode,
8992
nodes.JoinNode,
9093
nodes.AggregateNode,
94+
nodes.WindowOpNode,
9195
nodes.OrderByNode,
9296
nodes.ConcatNode,
9397
)
@@ -106,6 +110,8 @@ def _compile_node(self, node: bigframe_node.BigFrameNode) -> algebra_pb2.Rel:
106110
return self._compile_join(node)
107111
elif isinstance(node, nodes.AggregateNode):
108112
return self._compile_aggregate(node)
113+
elif isinstance(node, nodes.WindowOpNode):
114+
return self._compile_window(node)
109115
elif isinstance(node, nodes.OrderByNode):
110116
return self._compile_orderby(node)
111117
elif isinstance(node, nodes.ConcatNode):
@@ -307,22 +313,23 @@ def _compile_concat(self, node: nodes.ConcatNode) -> algebra_pb2.Rel:
307313
def _compile_aggregate(self, node: nodes.AggregateNode) -> algebra_pb2.Rel:
308314
input_rel = self._compile_node(node.child)
309315

316+
import bigframes.dtypes as dtypes
317+
import bigframes.operations.aggregations as agg_ops
318+
319+
child_ids = list(node.child.ids)
320+
310321
rel = algebra_pb2.Rel()
311322
agg_rel = rel.aggregate
312323
agg_rel.input.CopyFrom(input_rel)
313324

314325
if node.by_column_ids:
315326
grouping = agg_rel.groupings.add()
316327
for deref in node.by_column_ids:
317-
idx = list(node.child.ids).index(deref.id)
328+
idx = child_ids.index(deref.id)
318329
expr = grouping.grouping_expressions.add()
319330
expr.selection.direct_reference.struct_field.field = idx
320331

321-
import bigframes.dtypes as dtypes
322-
import bigframes.operations.aggregations as agg_ops
323-
324-
size_count = 0
325-
for agg, out_col_id in node.aggregations:
332+
for agg_idx, (agg, out_col_id) in enumerate(node.aggregations):
326333
distinct = False
327334
if isinstance(agg.op, agg_ops.SumOp):
328335
func_ref = self._EXTENSIONS["sum"]
@@ -331,7 +338,7 @@ def _compile_aggregate(self, node: nodes.AggregateNode) -> algebra_pb2.Rel:
331338
elif isinstance(agg.op, agg_ops.MinOp):
332339
func_ref = self._EXTENSIONS["min"]
333340
elif isinstance(agg.op, agg_ops.MeanOp):
334-
func_ref = self._EXTENSIONS["mean"]
341+
func_ref = self._EXTENSIONS["avg"]
335342
elif isinstance(agg.op, agg_ops.CountOp):
336343
func_ref = self._EXTENSIONS["count"]
337344
elif isinstance(agg.op, (agg_ops.SizeOp, agg_ops.SizeUnaryOp)):
@@ -366,38 +373,26 @@ def _compile_aggregate(self, node: nodes.AggregateNode) -> algebra_pb2.Rel:
366373

367374
measure = agg_rel.measures.add()
368375
measure.measure.function_reference = func_ref
376+
measure.measure.phase = algebra_pb2.AGGREGATION_PHASE_INITIAL_TO_RESULT
377+
378+
output_dtype = agg.output_type
379+
type_dict = self._convert_type(output_dtype)
380+
json_format.ParseDict(type_dict, measure.measure.output_type)
381+
369382
if distinct or isinstance(agg.op, agg_ops.NuniqueOp):
370383
measure.measure.invocation = (
371384
algebra_pb2.AggregateFunction.AGGREGATION_INVOCATION_DISTINCT
372385
)
373386

374-
if isinstance(agg.op, (agg_ops.SizeOp, agg_ops.SizeUnaryOp)):
375-
size_count += 1
376-
arg = measure.measure.arguments.add()
377-
arg.value.literal.i64 = size_count
378-
elif hasattr(agg, "column_references"):
387+
if hasattr(agg, "column_references"):
379388
for col_id in agg.column_references:
380389
try:
381-
idx = list(node.child.ids).index(col_id)
390+
idx = child_ids.index(col_id)
382391
field_expr = algebra_pb2.Expression()
383392
field_expr.selection.direct_reference.struct_field.field = idx
384-
385-
col_dtype = node.child.schema.items[idx].dtype
386-
is_bool = col_dtype == dtypes.BOOL_DTYPE
387-
if isinstance(
388-
agg.op, (agg_ops.StdOp, agg_ops.VarOp, agg_ops.PopVarOp)
389-
) or (
390-
isinstance(agg.op, (agg_ops.SumOp, agg_ops.MeanOp))
391-
and is_bool
392-
):
393-
casted_expr = self._compile_cast(
394-
field_expr, dtypes.FLOAT_DTYPE
395-
)
396-
arg = measure.measure.arguments.add()
397-
arg.value.CopyFrom(casted_expr)
398-
else:
399-
arg = measure.measure.arguments.add()
400-
arg.value.CopyFrom(field_expr)
393+
394+
arg = measure.measure.arguments.add()
395+
arg.value.CopyFrom(field_expr)
401396
except ValueError:
402397
pass
403398

@@ -411,6 +406,7 @@ def _compile_aggregate(self, node: nodes.AggregateNode) -> algebra_pb2.Rel:
411406
not_null_op.scalar_function.function_reference = self._EXTENSIONS[
412407
"is_not_null"
413408
]
409+
json_format.ParseDict({"bool": {}}, not_null_op.scalar_function.output_type)
414410
not_null_op.scalar_function.arguments.add().value.CopyFrom(key_expr)
415411
not_null_exprs.append(not_null_op)
416412

@@ -421,6 +417,7 @@ def _compile_aggregate(self, node: nodes.AggregateNode) -> algebra_pb2.Rel:
421417
and_expr.scalar_function.function_reference = self._EXTENSIONS[
422418
"and"
423419
]
420+
json_format.ParseDict({"bool": {}}, and_expr.scalar_function.output_type)
424421
and_expr.scalar_function.arguments.add().value.CopyFrom(expr)
425422
and_expr.scalar_function.arguments.add().value.CopyFrom(e)
426423
expr = and_expr
@@ -434,6 +431,184 @@ def _compile_aggregate(self, node: nodes.AggregateNode) -> algebra_pb2.Rel:
434431

435432
return rel
436433

434+
def _compile_window(self, node: nodes.WindowOpNode) -> algebra_pb2.Rel:
435+
input_rel = self._compile_node(node.child)
436+
437+
import bigframes.dtypes as dtypes
438+
import bigframes.operations.aggregations as agg_ops
439+
from bigframes.core import window_spec
440+
441+
child_ids = list(node.child.ids)
442+
443+
rel = algebra_pb2.Rel()
444+
proj = rel.project
445+
proj.input.CopyFrom(input_rel)
446+
447+
# 1. Project all child columns first
448+
for idx in range(len(child_ids)):
449+
expr = proj.expressions.add()
450+
expr.selection.direct_reference.struct_field.field = idx
451+
452+
# 2. Map window frame bounds (RowsWindowBounds / RangeWindowBounds / None)
453+
bounds_type = algebra_pb2.Expression.WindowFunction.BoundsType.BOUNDS_TYPE_UNSPECIFIED
454+
lower_bound = algebra_pb2.Expression.WindowFunction.Bound()
455+
upper_bound = algebra_pb2.Expression.WindowFunction.Bound()
456+
457+
if node.window_spec.bounds is not None:
458+
if isinstance(node.window_spec.bounds, window_spec.RowsWindowBounds):
459+
bounds_type = algebra_pb2.Expression.WindowFunction.BoundsType.BOUNDS_TYPE_ROWS
460+
461+
# Lower bound mapping
462+
start = node.window_spec.bounds.start
463+
if start is None:
464+
lower_bound.unbounded.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.Unbounded())
465+
elif start == 0:
466+
lower_bound.current_row.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.CurrentRow())
467+
elif start < 0:
468+
lower_bound.preceding.offset = -start
469+
else:
470+
lower_bound.following.offset = start
471+
472+
# Upper bound mapping
473+
end = node.window_spec.bounds.end
474+
if end is None:
475+
upper_bound.unbounded.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.Unbounded())
476+
elif end == 0:
477+
upper_bound.current_row.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.CurrentRow())
478+
elif end < 0:
479+
upper_bound.preceding.offset = -end
480+
else:
481+
upper_bound.following.offset = end
482+
483+
elif isinstance(node.window_spec.bounds, window_spec.RangeWindowBounds):
484+
bounds_type = algebra_pb2.Expression.WindowFunction.BoundsType.BOUNDS_TYPE_RANGE
485+
start = node.window_spec.bounds.start
486+
if start is None:
487+
lower_bound.unbounded.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.Unbounded())
488+
elif start == pd.Timedelta(0):
489+
lower_bound.current_row.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.CurrentRow())
490+
else:
491+
raise NotImplementedError("Range window bounds with non-zero offsets are not supported yet")
492+
493+
end = node.window_spec.bounds.end
494+
if end is None:
495+
upper_bound.unbounded.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.Unbounded())
496+
elif end == pd.Timedelta(0):
497+
upper_bound.current_row.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.CurrentRow())
498+
else:
499+
raise NotImplementedError("Range window bounds with non-zero offsets are not supported yet")
500+
else:
501+
bounds_type = algebra_pb2.Expression.WindowFunction.BoundsType.BOUNDS_TYPE_ROWS
502+
lower_bound.unbounded.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.Unbounded())
503+
upper_bound.unbounded.CopyFrom(algebra_pb2.Expression.WindowFunction.Bound.Unbounded())
504+
505+
# 3. Project each window aggregation expression as a WindowFunction expression
506+
for agg_idx, col_def in enumerate(node.agg_exprs):
507+
agg = col_def.expression
508+
distinct = False
509+
510+
if isinstance(agg.op, agg_ops.SumOp):
511+
func_ref = self._EXTENSIONS["sum"]
512+
elif isinstance(agg.op, agg_ops.MaxOp):
513+
func_ref = self._EXTENSIONS["max"]
514+
elif isinstance(agg.op, agg_ops.MinOp):
515+
func_ref = self._EXTENSIONS["min"]
516+
elif isinstance(agg.op, agg_ops.MeanOp):
517+
func_ref = self._EXTENSIONS["avg"]
518+
elif isinstance(agg.op, agg_ops.CountOp):
519+
func_ref = self._EXTENSIONS["count"]
520+
elif isinstance(agg.op, (agg_ops.SizeOp, agg_ops.SizeUnaryOp)):
521+
func_ref = self._EXTENSIONS["count"]
522+
elif isinstance(agg.op, agg_ops.NuniqueOp):
523+
func_ref = self._EXTENSIONS["count"]
524+
distinct = True
525+
elif isinstance(agg.op, agg_ops.StdOp):
526+
func_ref = self._EXTENSIONS["stddev"]
527+
elif isinstance(agg.op, agg_ops.VarOp):
528+
func_ref = self._EXTENSIONS["var"]
529+
elif isinstance(agg.op, agg_ops.PopVarOp):
530+
func_ref = self._EXTENSIONS["var_pop"]
531+
elif isinstance(agg.op, agg_ops.AnyValueOp):
532+
func_ref = self._EXTENSIONS["min"]
533+
elif isinstance(agg.op, agg_ops.AllOp):
534+
func_ref = self._EXTENSIONS["bool_and"]
535+
elif isinstance(agg.op, agg_ops.AnyOp):
536+
func_ref = self._EXTENSIONS["bool_or"]
537+
elif isinstance(agg.op, agg_ops.ProductOp):
538+
func_ref = self._EXTENSIONS["product"]
539+
elif isinstance(agg.op, agg_ops.MedianOp):
540+
func_ref = self._EXTENSIONS["median"]
541+
elif isinstance(agg.op, agg_ops.CovOp):
542+
func_ref = self._EXTENSIONS["cov"]
543+
elif isinstance(agg.op, agg_ops.CorrOp):
544+
func_ref = self._EXTENSIONS["corr"]
545+
else:
546+
raise NotImplementedError(
547+
f"Aggregation {type(agg.op)} not supported in window function yet"
548+
)
549+
550+
expr = proj.expressions.add()
551+
win_func = expr.window_function
552+
win_func.function_reference = func_ref
553+
win_func.phase = algebra_pb2.AGGREGATION_PHASE_INITIAL_TO_RESULT
554+
555+
bound_expr = ex.bind_schema_fields(agg, node.child.field_by_id)
556+
type_dict = self._convert_type(dtypes.dtype_for_etype(bound_expr.output_type))
557+
json_format.ParseDict(type_dict, win_func.output_type)
558+
559+
if distinct or isinstance(agg.op, agg_ops.NuniqueOp):
560+
win_func.invocation = (
561+
algebra_pb2.AggregateFunction.AGGREGATION_INVOCATION_DISTINCT
562+
)
563+
564+
# Set bounds
565+
win_func.lower_bound.CopyFrom(lower_bound)
566+
win_func.upper_bound.CopyFrom(upper_bound)
567+
win_func.bounds_type = bounds_type
568+
569+
# Set partitioning keys (partitions)
570+
for partition_expr in node.window_spec.grouping_keys:
571+
partition_pb = self._compile_expression(partition_expr, node.child)
572+
win_func.partitions.add().CopyFrom(partition_pb)
573+
574+
# Set sorting keys (sorts)
575+
for ord_expr in node.window_spec.ordering:
576+
sort_field = win_func.sorts.add()
577+
sort_pb = self._compile_expression(ord_expr.scalar_expression, node.child)
578+
sort_field.expr.CopyFrom(sort_pb)
579+
580+
is_asc = ord_expr.direction.is_ascending
581+
if is_asc:
582+
if ord_expr.na_last:
583+
sort_field.direction = algebra_pb2.SortField.SortDirection.SORT_DIRECTION_ASC_NULLS_LAST
584+
else:
585+
sort_field.direction = algebra_pb2.SortField.SortDirection.SORT_DIRECTION_ASC_NULLS_FIRST
586+
else:
587+
if ord_expr.na_last:
588+
sort_field.direction = algebra_pb2.SortField.SortDirection.SORT_DIRECTION_DESC_NULLS_LAST
589+
else:
590+
sort_field.direction = algebra_pb2.SortField.SortDirection.SORT_DIRECTION_DESC_NULLS_FIRST
591+
592+
# Set arguments
593+
if hasattr(agg, "column_references"):
594+
for col_id in agg.column_references:
595+
try:
596+
idx = child_ids.index(col_id)
597+
field_expr = algebra_pb2.Expression()
598+
field_expr.selection.direct_reference.struct_field.field = idx
599+
600+
arg = win_func.arguments.add()
601+
arg.value.CopyFrom(field_expr)
602+
except ValueError:
603+
pass
604+
605+
# Emit all columns (child columns + new window columns)
606+
proj.common.emit.output_mapping.extend(
607+
range(len(child_ids) + len(node.agg_exprs))
608+
)
609+
610+
return rel
611+
437612
def _compile_orderby(self, node: nodes.OrderByNode) -> algebra_pb2.Rel:
438613
input_rel = self._compile_node(node.child)
439614

@@ -478,7 +653,7 @@ def _compile_orderby(self, node: nodes.OrderByNode) -> algebra_pb2.Rel:
478653
"max": 12,
479654
"and": 13,
480655
"min": 14,
481-
"mean": 15,
656+
"avg": 15,
482657
"count": 16,
483658
"stddev": 17,
484659
"var": 18,

packages/bigframes/bigframes/core/rewrite/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
from bigframes.core.rewrite.schema_binding import bind_schema_to_tree
2929
from bigframes.core.rewrite.select_pullup import defer_selection
3030
from bigframes.core.rewrite.slices import pull_out_limit, pull_up_limits, rewrite_slice
31+
from bigframes.core.rewrite.substrait_agg import (
32+
rewrite_substrait_aggregations,
33+
rewrite_substrait_windows,
34+
)
3135
from bigframes.core.rewrite.timedeltas import rewrite_timedelta_expressions
3236
from bigframes.core.rewrite.udfs import lower_udfs
3337
from bigframes.core.rewrite.windows import (
@@ -43,6 +47,8 @@
4347
"legacy_join_as_projection",
4448
"try_row_join",
4549
"rewrite_slice",
50+
"rewrite_substrait_aggregations",
51+
"rewrite_substrait_windows",
4652
"rewrite_timedelta_expressions",
4753
"pull_up_limits",
4854
"pull_out_limit",

0 commit comments

Comments
 (0)