Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.
17 changes: 17 additions & 0 deletions tests/system/small/test_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -6296,3 +6296,20 @@ def test_agg_with_dict_containing_non_existing_col_raise_key_error(scalars_dfs):

with pytest.raises(KeyError):
bf_df.agg(agg_funcs)


def test_empty_agg_projection_succeeds():
# Tests that the compiler generates a SELECT 1 fallback for empty aggregations,
# protecting against BigQuery syntax errors when both groups and metrics are empty.
import importlib

bq = importlib.import_module(
"bigframes_vendored.ibis.backends.sql.compilers.bigquery"
)
sg = importlib.import_module("bigframes_vendored.sqlglot")

compiler = bq.BigQueryCompiler()
res = compiler.visit_Aggregate(
"op", parent=sg.table("parent_table"), groups=[], metrics=[]
)
assert "SELECT 1" in res.sql()
Original file line number Diff line number Diff line change
Expand Up @@ -1394,9 +1394,17 @@ def _generate_groups(groups):
return map(sge.convert, range(1, len(groups) + 1))

def visit_Aggregate(self, op, *, parent, groups, metrics):
sel = sg.select(
*self._cleanup_names(groups), *self._cleanup_names(metrics), copy=False
).from_(parent, copy=False)
exprs = []
if groups:
exprs.extend(self._cleanup_names(groups))
if metrics:
exprs.extend(self._cleanup_names(metrics))

if not exprs:
# Empty aggregated projections are invalid in BigQuery
exprs = [sge.Literal.number(1)]

sel = sg.select(*exprs, copy=False).from_(parent, copy=False)

if groups:
sel = sel.group_by(*self._generate_groups(groups.values()), copy=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,15 @@ def visit_TimestampFromUNIX(self, op, *, arg, unit):

def visit_Cast(self, op, *, arg, to):
from_ = op.arg.dtype
if to.is_null():
return sge.Null()
if arg is NULL or (
isinstance(arg, sge.Cast)
and getattr(arg, "to", None) is not None
and str(arg.to).upper() == "NULL"
):
if to.is_struct() or to.is_array():
return sge.Cast(this=NULL, to=self.type_mapper.from_ibis(to))
if from_.is_timestamp() and to.is_integer():
return self.f.unix_micros(arg)
elif from_.is_integer() and to.is_timestamp():
Expand Down
Loading