@@ -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 ,
0 commit comments