2929from google .cloud import bigquery
3030import pyarrow as pa
3131
32+ from bigframes .core import utils
3233import bigframes .core .compile .aggregate_compiler as agg_compiler
3334import bigframes .core .compile .googlesql
3435import bigframes .core .compile .ibis_types
@@ -231,7 +232,7 @@ def aggregate(
231232 col_out : agg_compiler .compile_aggregate (
232233 aggregate ,
233234 bindings ,
234- order_by = _convert_ordering_to_table_values (table , order_by ),
235+ order_by = _convert_row_ordering_to_table_values (table , order_by ),
235236 )
236237 for aggregate , col_out in aggregations
237238 }
@@ -439,7 +440,7 @@ def project_window_op(
439440 never_skip_nulls = never_skip_nulls ,
440441 )
441442
442- if expression .op .order_independent and not window_spec .row_bounded :
443+ if expression .op .order_independent and window_spec .is_unbounded :
443444 # notably percentile_cont does not support ordering clause
444445 window_spec = window_spec .without_order ()
445446 window = self ._ibis_window_from_spec (window_spec )
@@ -517,16 +518,30 @@ def _ibis_window_from_spec(self, window_spec: WindowSpec):
517518 # 1. Order-independent op (aggregation, cut, rank) with unbound window - no ordering clause needed
518519 # 2. Order-independent op (aggregation, cut, rank) with range window - use ordering clause, ties allowed
519520 # 3. Order-depedenpent op (navigation functions, array_agg) or rows bounds - use total row order to break ties.
520- if window_spec .ordering :
521- order_by = _convert_ordering_to_table_values (
521+ if window_spec .is_row_bounded :
522+ if not window_spec .ordering :
523+ # If window spec has following or preceding bounds, we need to apply an unambiguous ordering.
524+ raise ValueError ("No ordering provided for ordered analytic function" )
525+ order_by = _convert_row_ordering_to_table_values (
522526 self ._column_names ,
523527 window_spec .ordering ,
524528 )
525- elif window_spec .row_bounded :
526- # If window spec has following or preceding bounds, we need to apply an unambiguous ordering.
527- raise ValueError ("No ordering provided for ordered analytic function" )
528- else :
529+
530+ elif window_spec .is_range_bounded :
531+ order_by = [
532+ _convert_range_ordering_to_table_value (
533+ self ._column_names ,
534+ window_spec .ordering [0 ],
535+ )
536+ ]
537+ # The rest if branches are for unbounded windows
538+ elif window_spec .ordering :
529539 # Unbound grouping window. Suitable for aggregations but not for analytic function application.
540+ order_by = _convert_row_ordering_to_table_values (
541+ self ._column_names ,
542+ window_spec .ordering ,
543+ )
544+ else :
530545 order_by = None
531546
532547 window = bigframes_vendored .ibis .window (order_by = order_by , group_by = group_by )
@@ -551,7 +566,7 @@ def is_window(column: ibis_types.Value) -> bool:
551566 return any (isinstance (op , ibis_ops .WindowFunction ) for op in matches )
552567
553568
554- def _convert_ordering_to_table_values (
569+ def _convert_row_ordering_to_table_values (
555570 value_lookup : typing .Mapping [str , ibis_types .Value ],
556571 ordering_columns : typing .Sequence [OrderingExpression ],
557572) -> typing .Sequence [ibis_types .Value ]:
@@ -579,6 +594,30 @@ def _convert_ordering_to_table_values(
579594 return ordering_values
580595
581596
597+ def _convert_range_ordering_to_table_value (
598+ value_lookup : typing .Mapping [str , ibis_types .Value ],
599+ ordering_column : OrderingExpression ,
600+ ) -> ibis_types .Value :
601+ """Converts the ordering for range windows to Ibis references.
602+
603+ Note that this method is different from `_convert_row_ordering_to_table_values` in
604+ that it does not arrange null values. There are two reasons:
605+ 1. Manipulating null positions requires more than one ordering key, which is forbidden
606+ by SQL window syntax for range rolling.
607+ 2. Pandas does not allow range rolling on timeseries with nulls.
608+
609+ Therefore, we opt for the simplest approach here: generate the simplest SQL and follow
610+ the BigQuery engine behavior.
611+ """
612+ expr = op_compiler .compile_expression (
613+ ordering_column .scalar_expression , value_lookup
614+ )
615+
616+ if ordering_column .direction .is_ascending :
617+ return bigframes_vendored .ibis .asc (expr ) # type: ignore
618+ return bigframes_vendored .ibis .desc (expr ) # type: ignore
619+
620+
582621def _string_cast_join_cond (
583622 lvalue : ibis_types .Column , rvalue : ibis_types .Column
584623) -> ibis_types .BooleanColumn :
@@ -668,8 +707,14 @@ def _add_boundary(
668707) -> ibis_expr_builders .LegacyWindowBuilder :
669708 if isinstance (bounds , RangeWindowBounds ):
670709 return ibis_window .range (
671- start = _to_ibis_boundary (bounds .start ),
672- end = _to_ibis_boundary (bounds .end ),
710+ start = _to_ibis_boundary (
711+ None
712+ if bounds .start is None
713+ else utils .timedelta_to_micros (bounds .start )
714+ ),
715+ end = _to_ibis_boundary (
716+ None if bounds .end is None else utils .timedelta_to_micros (bounds .end )
717+ ),
673718 )
674719 if isinstance (bounds , RowsWindowBounds ):
675720 if bounds .start is not None or bounds .end is not None :
0 commit comments