Skip to content

Commit 63a83d7

Browse files
Merge remote-tracking branch 'github/main' into fix_solo_if_else
2 parents 89a66dd + a3c2522 commit 63a83d7

File tree

20 files changed

+356
-38
lines changed

20 files changed

+356
-38
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@
44

55
[1]: https://pypi.org/project/bigframes/#history
66

7+
## [2.22.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.21.0...v2.22.0) (2025-09-25)
8+
9+
10+
### Features
11+
12+
* Add `GroupBy.__iter__` ([#1394](https://github.com/googleapis/python-bigquery-dataframes/issues/1394)) ([c56a78c](https://github.com/googleapis/python-bigquery-dataframes/commit/c56a78cd509a535d4998d5b9a99ec3ecd334b883))
13+
* Add ai.generate_int to bigframes.bigquery package ([#2109](https://github.com/googleapis/python-bigquery-dataframes/issues/2109)) ([af6b862](https://github.com/googleapis/python-bigquery-dataframes/commit/af6b862de5c3921684210ec169338815f45b19dd))
14+
* Add Groupby.describe() ([#2088](https://github.com/googleapis/python-bigquery-dataframes/issues/2088)) ([328a765](https://github.com/googleapis/python-bigquery-dataframes/commit/328a765e746138806a021bea22475e8c03512aeb))
15+
* Implement `Index.to_list()` ([#2106](https://github.com/googleapis/python-bigquery-dataframes/issues/2106)) ([60056ca](https://github.com/googleapis/python-bigquery-dataframes/commit/60056ca06511f99092647fe55fc02eeab486b4ca))
16+
* Implement inplace parameter for `DataFrame.drop` ([#2105](https://github.com/googleapis/python-bigquery-dataframes/issues/2105)) ([3487f13](https://github.com/googleapis/python-bigquery-dataframes/commit/3487f13d12e34999b385c2e11551b5e27bfbf4ff))
17+
* Support callable for series map method ([#2100](https://github.com/googleapis/python-bigquery-dataframes/issues/2100)) ([ac25618](https://github.com/googleapis/python-bigquery-dataframes/commit/ac25618feed2da11fe4fb85058d498d262c085c0))
18+
* Support df.info() with null index ([#2094](https://github.com/googleapis/python-bigquery-dataframes/issues/2094)) ([fb81eea](https://github.com/googleapis/python-bigquery-dataframes/commit/fb81eeaf13af059f32cb38e7f117fb3504243d51))
19+
20+
21+
### Bug Fixes
22+
23+
* Avoid ibis fillna warning in compiler ([#2113](https://github.com/googleapis/python-bigquery-dataframes/issues/2113)) ([7ef667b](https://github.com/googleapis/python-bigquery-dataframes/commit/7ef667b0f46f13bcc8ad4f2ed8f81278132b5aec))
24+
* Negative start and stop parameter values in Series.str.slice() ([#2104](https://github.com/googleapis/python-bigquery-dataframes/issues/2104)) ([f57a348](https://github.com/googleapis/python-bigquery-dataframes/commit/f57a348f1935a4e2bb14c501bb4c47cd552d102a))
25+
* Throw type error for incomparable join keys ([#2098](https://github.com/googleapis/python-bigquery-dataframes/issues/2098)) ([9dc9695](https://github.com/googleapis/python-bigquery-dataframes/commit/9dc96959a84b751d18b290129c2926df6e50b3f5))
26+
* Transformers with non-standard column names throw errors ([#2089](https://github.com/googleapis/python-bigquery-dataframes/issues/2089)) ([a2daa3f](https://github.com/googleapis/python-bigquery-dataframes/commit/a2daa3fffe6743327edb9f4c74db93198bd12f8e))
27+
728
## [2.21.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.20.0...v2.21.0) (2025-09-17)
829

930

bigframes/core/compile/default_ordering.py renamed to bigframes/core/compile/ibis_compiler/default_ordering.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ def _convert_to_nonnull_string(column: ibis_types.Value) -> ibis_types.StringVal
4747
result = ibis_ops.ToJsonString(column).to_expr() # type: ignore
4848
# Escape backslashes and use backslash as delineator
4949
escaped = cast(
50-
ibis_types.StringColumn,
51-
result.fill_null(ibis_types.literal(""))
52-
if hasattr(result, "fill_null")
53-
else result.fillna(""),
50+
ibis_types.StringColumn, result.fill_null(ibis_types.literal(""))
5451
).replace(
5552
"\\", # type: ignore
5653
"\\\\", # type: ignore

bigframes/core/compile/ibis_compiler/scalar_op_registry.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import pandas as pd
2929

3030
from bigframes.core.compile.constants import UNIT_TO_US_CONVERSION_FACTORS
31-
import bigframes.core.compile.default_ordering
31+
import bigframes.core.compile.ibis_compiler.default_ordering
3232
from bigframes.core.compile.ibis_compiler.scalar_op_compiler import (
3333
scalar_op_compiler, # TODO(tswast): avoid import of variables
3434
)
@@ -1064,7 +1064,7 @@ def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp):
10641064
if op.match_nulls and contains_nulls:
10651065
return x.isnull() | x.isin(matchable_ibis_values)
10661066
else:
1067-
return x.isin(matchable_ibis_values).fillna(False)
1067+
return x.isin(matchable_ibis_values).fill_null(ibis.literal(False))
10681068

10691069

10701070
@scalar_op_compiler.register_unary_op(ops.ToDatetimeOp, pass_op=True)
@@ -1387,8 +1387,8 @@ def eq_nulls_match_op(
13871387
left = x.cast(ibis_dtypes.str).fill_null(literal)
13881388
right = y.cast(ibis_dtypes.str).fill_null(literal)
13891389
else:
1390-
left = x.cast(ibis_dtypes.str).fillna(literal)
1391-
right = y.cast(ibis_dtypes.str).fillna(literal)
1390+
left = x.cast(ibis_dtypes.str).fill_null(literal)
1391+
right = y.cast(ibis_dtypes.str).fill_null(literal)
13921392

13931393
return left == right
13941394

@@ -1817,7 +1817,7 @@ def fillna_op(
18171817
if hasattr(x, "fill_null"):
18181818
return x.fill_null(typing.cast(ibis_types.Scalar, y))
18191819
else:
1820-
return x.fillna(typing.cast(ibis_types.Scalar, y))
1820+
return x.fill_null(typing.cast(ibis_types.Scalar, y))
18211821

18221822

18231823
@scalar_op_compiler.register_binary_op(ops.round_op)
@@ -2020,7 +2020,7 @@ def _construct_prompt(
20202020

20212021
@scalar_op_compiler.register_nary_op(ops.RowKey, pass_op=True)
20222022
def rowkey_op_impl(*values: ibis_types.Value, op: ops.RowKey) -> ibis_types.Value:
2023-
return bigframes.core.compile.default_ordering.gen_row_key(values)
2023+
return bigframes.core.compile.ibis_compiler.default_ordering.gen_row_key(values)
20242024

20252025

20262026
# Helpers

bigframes/core/compile/sqlglot/aggregations/op_registration.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,16 @@ def arg_checker(*args, **kwargs):
4141
)
4242
return item(*args, **kwargs)
4343

44-
if hasattr(op, "name"):
45-
key = typing.cast(str, op.name)
46-
if key in self._registered_ops:
47-
raise ValueError(f"{key} is already registered")
48-
else:
49-
raise ValueError(f"The operator must have a 'name' attribute. Got {op}")
44+
key = str(op)
45+
if key in self._registered_ops:
46+
raise ValueError(f"{key} is already registered")
5047
self._registered_ops[key] = item
5148
return arg_checker
5249

5350
return decorator
5451

5552
def __getitem__(self, op: str | agg_ops.WindowOp) -> CompilationFunc:
56-
if isinstance(op, agg_ops.WindowOp):
57-
if not hasattr(op, "name"):
58-
raise ValueError(f"The operator must have a 'name' attribute. Got {op}")
59-
else:
60-
key = typing.cast(str, op.name)
61-
return self._registered_ops[key]
62-
return self._registered_ops[op]
53+
key = op if isinstance(op, type) else type(op)
54+
if str(key) not in self._registered_ops:
55+
raise ValueError(f"{key} is already not registered")
56+
return self._registered_ops[str(key)]

bigframes/core/compile/sqlglot/aggregations/unary_compiler.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,37 @@ def compile(
3838
return UNARY_OP_REGISTRATION[op](op, column, window=window)
3939

4040

41+
@UNARY_OP_REGISTRATION.register(agg_ops.ApproxQuartilesOp)
42+
def _(
43+
op: agg_ops.ApproxQuartilesOp,
44+
column: typed_expr.TypedExpr,
45+
window: typing.Optional[window_spec.WindowSpec] = None,
46+
) -> sge.Expression:
47+
if window is not None:
48+
raise NotImplementedError("Approx Quartiles with windowing is not supported.")
49+
# APPROX_QUANTILES returns an array of the quartiles, so we need to index it.
50+
# The op.quartile is 1-based for the quartile, but array is 0-indexed.
51+
# The quartiles are Q0, Q1, Q2, Q3, Q4. op.quartile is 1, 2, or 3.
52+
# The array has 5 elements (for N=4 intervals).
53+
# So we want the element at index `op.quartile`.
54+
approx_quantiles_expr = sge.func("APPROX_QUANTILES", column.expr, sge.convert(4))
55+
return sge.Bracket(
56+
this=approx_quantiles_expr,
57+
expressions=[sge.func("OFFSET", sge.convert(op.quartile))],
58+
)
59+
60+
61+
@UNARY_OP_REGISTRATION.register(agg_ops.ApproxTopCountOp)
62+
def _(
63+
op: agg_ops.ApproxTopCountOp,
64+
column: typed_expr.TypedExpr,
65+
window: typing.Optional[window_spec.WindowSpec] = None,
66+
) -> sge.Expression:
67+
if window is not None:
68+
raise NotImplementedError("Approx top count with windowing is not supported.")
69+
return sge.func("APPROX_TOP_COUNT", column.expr, sge.convert(op.number))
70+
71+
4172
@UNARY_OP_REGISTRATION.register(agg_ops.CountOp)
4273
def _(
4374
op: agg_ops.CountOp,
@@ -47,6 +78,18 @@ def _(
4778
return apply_window_if_present(sge.func("COUNT", column.expr), window)
4879

4980

81+
@UNARY_OP_REGISTRATION.register(agg_ops.DenseRankOp)
82+
def _(
83+
op: agg_ops.DenseRankOp,
84+
column: typed_expr.TypedExpr,
85+
window: typing.Optional[window_spec.WindowSpec] = None,
86+
) -> sge.Expression:
87+
# Ranking functions do not support window framing clauses.
88+
return apply_window_if_present(
89+
sge.func("DENSE_RANK"), window, include_framing_clauses=False
90+
)
91+
92+
5093
@UNARY_OP_REGISTRATION.register(agg_ops.MaxOp)
5194
def _(
5295
op: agg_ops.MaxOp,
@@ -56,6 +99,26 @@ def _(
5699
return apply_window_if_present(sge.func("MAX", column.expr), window)
57100

58101

102+
@UNARY_OP_REGISTRATION.register(agg_ops.MeanOp)
103+
def _(
104+
op: agg_ops.MeanOp,
105+
column: typed_expr.TypedExpr,
106+
window: typing.Optional[window_spec.WindowSpec] = None,
107+
) -> sge.Expression:
108+
expr = column.expr
109+
if column.dtype == dtypes.BOOL_DTYPE:
110+
expr = sge.Cast(this=expr, to="INT64")
111+
112+
expr = sge.func("AVG", expr)
113+
114+
should_floor_result = (
115+
op.should_floor_result or column.dtype == dtypes.TIMEDELTA_DTYPE
116+
)
117+
if should_floor_result:
118+
expr = sge.Cast(this=sge.func("FLOOR", expr), to="INT64")
119+
return apply_window_if_present(expr, window)
120+
121+
59122
@UNARY_OP_REGISTRATION.register(agg_ops.MedianOp)
60123
def _(
61124
op: agg_ops.MedianOp,
@@ -77,6 +140,37 @@ def _(
77140
return apply_window_if_present(sge.func("MIN", column.expr), window)
78141

79142

143+
@UNARY_OP_REGISTRATION.register(agg_ops.QuantileOp)
144+
def _(
145+
op: agg_ops.QuantileOp,
146+
column: typed_expr.TypedExpr,
147+
window: typing.Optional[window_spec.WindowSpec] = None,
148+
) -> sge.Expression:
149+
# TODO: Support interpolation argument
150+
# TODO: Support percentile_disc
151+
result: sge.Expression = sge.func("PERCENTILE_CONT", column.expr, sge.convert(op.q))
152+
if window is None:
153+
# PERCENTILE_CONT is a navigation function, not an aggregate function, so it always needs an OVER clause.
154+
result = sge.Window(this=result)
155+
else:
156+
result = apply_window_if_present(result, window)
157+
if op.should_floor_result:
158+
result = sge.Cast(this=sge.func("FLOOR", result), to="INT64")
159+
return result
160+
161+
162+
@UNARY_OP_REGISTRATION.register(agg_ops.RankOp)
163+
def _(
164+
op: agg_ops.RankOp,
165+
column: typed_expr.TypedExpr,
166+
window: typing.Optional[window_spec.WindowSpec] = None,
167+
) -> sge.Expression:
168+
# Ranking functions do not support window framing clauses.
169+
return apply_window_if_present(
170+
sge.func("RANK"), window, include_framing_clauses=False
171+
)
172+
173+
80174
@UNARY_OP_REGISTRATION.register(agg_ops.SizeUnaryOp)
81175
def _(
82176
op: agg_ops.SizeUnaryOp,

bigframes/core/compile/sqlglot/aggregations/windows.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
def apply_window_if_present(
2626
value: sge.Expression,
2727
window: typing.Optional[window_spec.WindowSpec] = None,
28+
include_framing_clauses: bool = True,
2829
) -> sge.Expression:
2930
if window is None:
3031
return value
@@ -64,6 +65,9 @@ def apply_window_if_present(
6465
if not window.bounds and not order:
6566
return sge.Window(this=value, partition_by=group_by)
6667

68+
if not window.bounds and not include_framing_clauses:
69+
return sge.Window(this=value, partition_by=group_by, order=order)
70+
6771
kind = (
6872
"ROWS" if isinstance(window.bounds, window_spec.RowsWindowBounds) else "RANGE"
6973
)

bigframes/operations/aggregations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,8 @@ def implicitly_inherits_order(self):
519519

520520
@dataclasses.dataclass(frozen=True)
521521
class DenseRankOp(UnaryWindowOp):
522+
name: ClassVar[str] = "dense_rank"
523+
522524
@property
523525
def skips_nulls(self):
524526
return False

bigframes/session/_io/bigquery/read_gbq_table.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,9 @@
2727
import google.api_core.exceptions
2828
import google.cloud.bigquery as bigquery
2929

30-
import bigframes.clients
31-
import bigframes.core.compile
32-
import bigframes.core.compile.default_ordering
3330
import bigframes.core.sql
34-
import bigframes.dtypes
3531
import bigframes.exceptions as bfe
3632
import bigframes.session._io.bigquery
37-
import bigframes.session.clients
38-
import bigframes.version
3933

4034
# Avoid circular imports.
4135
if typing.TYPE_CHECKING:

bigframes/version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
__version__ = "2.21.0"
15+
__version__ = "2.22.0"
1616

1717
# {x-release-please-start-date}
18-
__release_date__ = "2025-09-17"
18+
__release_date__ = "2025-09-25"
1919
# {x-release-please-end}

tests/system/small/engines/test_aggregation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_engines_aggregate_size(
7171
assert_equivalence_execution(node, REFERENCE_ENGINE, engine)
7272

7373

74-
@pytest.mark.parametrize("engine", ["polars", "bq"], indirect=True)
74+
@pytest.mark.parametrize("engine", ["polars", "bq", "bq-sqlglot"], indirect=True)
7575
@pytest.mark.parametrize(
7676
"op",
7777
[agg_ops.min_op, agg_ops.max_op, agg_ops.mean_op, agg_ops.sum_op, agg_ops.count_op],

0 commit comments

Comments
 (0)