Skip to content

Commit 360edb3

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor-isnull-op
2 parents 65e9fd4 + 24050cb commit 360edb3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1865
-481
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,8 @@ repos:
4242
additional_dependencies: [types-requests, types-tabulate, types-PyYAML, pandas-stubs<=2.2.3.241126]
4343
exclude: "^third_party"
4444
args: ["--check-untyped-defs", "--explicit-package-bases", "--ignore-missing-imports"]
45+
- repo: https://github.com/biomejs/pre-commit
46+
rev: v2.0.2
47+
hooks:
48+
- id: biome-check
49+
files: '\.js$'

CHANGELOG.md

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

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

7+
## [2.10.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.9.0...v2.10.0) (2025-07-08)
8+
9+
10+
### Features
11+
12+
* `df.to_pandas_batches()` returns one empty DataFrame if `df` is empty ([#1878](https://github.com/googleapis/python-bigquery-dataframes/issues/1878)) ([e43d15d](https://github.com/googleapis/python-bigquery-dataframes/commit/e43d15d535d6d5fd73c33967271f3591c41dffb3))
13+
* Add filter pushdown to hybrid engine ([#1871](https://github.com/googleapis/python-bigquery-dataframes/issues/1871)) ([6454aff](https://github.com/googleapis/python-bigquery-dataframes/commit/6454aff726dee791acbac98f893075ee5ee6d9a1))
14+
* Add simple stats support to hybrid local pushdown ([#1873](https://github.com/googleapis/python-bigquery-dataframes/issues/1873)) ([8715105](https://github.com/googleapis/python-bigquery-dataframes/commit/8715105239216bffe899ddcbb15805f2e3063af4))
15+
16+
17+
### Bug Fixes
18+
19+
* Fix issues where duration type returned as int ([#1875](https://github.com/googleapis/python-bigquery-dataframes/issues/1875)) ([f30f750](https://github.com/googleapis/python-bigquery-dataframes/commit/f30f75053a6966abd1a6a644c23efb86b2ac568d))
20+
21+
22+
### Documentation
23+
24+
* Update gsutil commands to gcloud commands ([#1876](https://github.com/googleapis/python-bigquery-dataframes/issues/1876)) ([c289f70](https://github.com/googleapis/python-bigquery-dataframes/commit/c289f7061320ec6d9de099cab2416cc9f289baac))
25+
726
## [2.9.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.8.0...v2.9.0) (2025-06-30)
827

928

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# Generated by synthtool. DO NOT EDIT!
1818
include README.rst LICENSE
1919
recursive-include third_party/bigframes_vendored *
20-
recursive-include bigframes *.json *.proto py.typed
20+
recursive-include bigframes *.json *.proto *.js py.typed
2121
recursive-include tests *
2222
global-exclude *.py[co]
2323
global-exclude __pycache__

bigframes/core/block_transforms.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,8 @@ def rank(
522522
def dropna(
523523
block: blocks.Block,
524524
column_ids: typing.Sequence[str],
525-
how: typing.Literal["all", "any"] = "any",
525+
how: str = "any",
526+
thresh: typing.Optional[int] = None,
526527
subset: Optional[typing.Sequence[str]] = None,
527528
):
528529
"""
@@ -531,17 +532,38 @@ def dropna(
531532
if subset is None:
532533
subset = column_ids
533534

535+
# Predicates to check for non-null values in the subset of columns
534536
predicates = [
535537
ops.notnull_op.as_expr(column_id)
536538
for column_id in column_ids
537539
if column_id in subset
538540
]
541+
539542
if len(predicates) == 0:
540543
return block
541-
if how == "any":
542-
predicate = functools.reduce(ops.and_op.as_expr, predicates)
543-
else: # "all"
544-
predicate = functools.reduce(ops.or_op.as_expr, predicates)
544+
545+
if thresh is not None:
546+
# Handle single predicate case
547+
if len(predicates) == 1:
548+
count_expr = ops.AsTypeOp(pd.Int64Dtype()).as_expr(predicates[0])
549+
else:
550+
# Sum the boolean expressions to count non-null values
551+
count_expr = functools.reduce(
552+
lambda a, b: ops.add_op.as_expr(
553+
ops.AsTypeOp(pd.Int64Dtype()).as_expr(a),
554+
ops.AsTypeOp(pd.Int64Dtype()).as_expr(b),
555+
),
556+
predicates,
557+
)
558+
# Filter rows where count >= thresh
559+
predicate = ops.ge_op.as_expr(count_expr, ex.const(thresh))
560+
else:
561+
# Only handle 'how' parameter when thresh is not specified
562+
if how == "any":
563+
predicate = functools.reduce(ops.and_op.as_expr, predicates)
564+
else: # "all"
565+
predicate = functools.reduce(ops.or_op.as_expr, predicates)
566+
545567
return block.filter(predicate)
546568

547569

bigframes/core/blocks.py

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@
2929
import random
3030
import textwrap
3131
import typing
32-
from typing import Iterable, List, Literal, Mapping, Optional, Sequence, Tuple, Union
32+
from typing import (
33+
Iterable,
34+
Iterator,
35+
List,
36+
Literal,
37+
Mapping,
38+
Optional,
39+
Sequence,
40+
Tuple,
41+
Union,
42+
)
3343
import warnings
3444

3545
import bigframes_vendored.constants as constants
@@ -87,14 +97,22 @@
8797
LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]]
8898

8999

90-
class BlockHolder(typing.Protocol):
100+
@dataclasses.dataclass
101+
class PandasBatches(Iterator[pd.DataFrame]):
91102
"""Interface for mutable objects with state represented by a block value object."""
92103

93-
def _set_block(self, block: Block):
94-
"""Set the underlying block value of the object"""
104+
def __init__(
105+
self, pandas_batches: Iterator[pd.DataFrame], total_rows: Optional[int] = 0
106+
):
107+
self._dataframes: Iterator[pd.DataFrame] = pandas_batches
108+
self._total_rows: Optional[int] = total_rows
109+
110+
@property
111+
def total_rows(self) -> Optional[int]:
112+
return self._total_rows
95113

96-
def _get_block(self) -> Block:
97-
"""Get the underlying block value of the object"""
114+
def __next__(self) -> pd.DataFrame:
115+
return next(self._dataframes)
98116

99117

100118
@dataclasses.dataclass()
@@ -599,8 +617,7 @@ def try_peek(
599617
self.expr, n, use_explicit_destination=allow_large_results
600618
)
601619
df = result.to_pandas()
602-
self._copy_index_to_pandas(df)
603-
return df
620+
return self._copy_index_to_pandas(df)
604621
else:
605622
return None
606623

@@ -609,8 +626,7 @@ def to_pandas_batches(
609626
page_size: Optional[int] = None,
610627
max_results: Optional[int] = None,
611628
allow_large_results: Optional[bool] = None,
612-
squeeze: Optional[bool] = False,
613-
):
629+
) -> Iterator[pd.DataFrame]:
614630
"""Download results one message at a time.
615631
616632
page_size and max_results determine the size and number of batches,
@@ -621,43 +637,43 @@ def to_pandas_batches(
621637
use_explicit_destination=allow_large_results,
622638
)
623639

624-
total_batches = 0
625-
for df in execute_result.to_pandas_batches(
626-
page_size=page_size, max_results=max_results
627-
):
628-
total_batches += 1
629-
self._copy_index_to_pandas(df)
630-
if squeeze:
631-
yield df.squeeze(axis=1)
632-
else:
633-
yield df
634-
635640
# To reduce the number of edge cases to consider when working with the
636641
# results of this, always return at least one DataFrame. See:
637642
# b/428918844.
638-
if total_batches == 0:
639-
df = pd.DataFrame(
640-
{
641-
col: pd.Series([], dtype=self.expr.get_column_type(col))
642-
for col in itertools.chain(self.value_columns, self.index_columns)
643-
}
644-
)
645-
self._copy_index_to_pandas(df)
646-
yield df
643+
empty_val = pd.DataFrame(
644+
{
645+
col: pd.Series([], dtype=self.expr.get_column_type(col))
646+
for col in itertools.chain(self.value_columns, self.index_columns)
647+
}
648+
)
649+
dfs = map(
650+
lambda a: a[0],
651+
itertools.zip_longest(
652+
execute_result.to_pandas_batches(page_size, max_results),
653+
[0],
654+
fillvalue=empty_val,
655+
),
656+
)
657+
dfs = iter(map(self._copy_index_to_pandas, dfs))
647658

648-
def _copy_index_to_pandas(self, df: pd.DataFrame):
649-
"""Set the index on pandas DataFrame to match this block.
659+
total_rows = execute_result.total_rows
660+
if (total_rows is not None) and (max_results is not None):
661+
total_rows = min(total_rows, max_results)
650662

651-
Warning: This method modifies ``df`` inplace.
652-
"""
663+
return PandasBatches(dfs, total_rows)
664+
665+
def _copy_index_to_pandas(self, df: pd.DataFrame) -> pd.DataFrame:
666+
"""Set the index on pandas DataFrame to match this block."""
653667
# Note: If BigQuery DataFrame has null index, a default one will be created for the local materialization.
668+
new_df = df.copy()
654669
if len(self.index_columns) > 0:
655-
df.set_index(list(self.index_columns), inplace=True)
670+
new_df.set_index(list(self.index_columns), inplace=True)
656671
# Pandas names is annotated as list[str] rather than the more
657672
# general Sequence[Label] that BigQuery DataFrames has.
658673
# See: https://github.com/pandas-dev/pandas-stubs/issues/804
659-
df.index.names = self.index.names # type: ignore
660-
df.columns = self.column_labels
674+
new_df.index.names = self.index.names # type: ignore
675+
new_df.columns = self.column_labels
676+
return new_df
661677

662678
def _materialize_local(
663679
self, materialize_options: MaterializationOptions = MaterializationOptions()
@@ -724,9 +740,7 @@ def _materialize_local(
724740
)
725741
else:
726742
df = execute_result.to_pandas()
727-
self._copy_index_to_pandas(df)
728-
729-
return df, execute_result.query_job
743+
return self._copy_index_to_pandas(df), execute_result.query_job
730744

731745
def _downsample(
732746
self, total_rows: int, sampling_method: str, fraction: float, random_state
@@ -1591,8 +1605,7 @@ def retrieve_repr_request_results(
15911605
row_count = self.session._executor.execute(self.expr.row_count()).to_py_scalar()
15921606

15931607
head_df = head_result.to_pandas()
1594-
self._copy_index_to_pandas(head_df)
1595-
return head_df, row_count, head_result.query_job
1608+
return self._copy_index_to_pandas(head_df), row_count, head_result.query_job
15961609

15971610
def promote_offsets(self, label: Label = None) -> typing.Tuple[Block, str]:
15981611
expr, result_id = self._expr.promote_offsets()

0 commit comments

Comments
 (0)