Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a2812c9
feat: implement pandas-gbq delegation check and telemetry
shuoweil Jul 13, 2026
206ab7f
test: refactor delegation tests to comply with unit test style guide
shuoweil Jul 13, 2026
7646f22
Update packages/google-cloud-bigquery/tests/unit/test_table.py
shuoweil Jul 13, 2026
0af9073
refactor: harden version checks and user-agent creation based on PR f…
shuoweil Jul 13, 2026
be85288
test: ignore PendingDeprecationWarning in tqdm error test
shuoweil Jul 13, 2026
a74b5c0
style: run black formatting on modified files
shuoweil Jul 13, 2026
65f78bc
test: skip delegation tests if db_dtypes is not installed
shuoweil Jul 13, 2026
d91d822
test: skip range pyarrow delegation test if pandas lacks ArrowDtype
shuoweil Jul 14, 2026
09f0490
test: add comprehensive unit tests for PandasGBQVersions to fix coverage
shuoweil Jul 14, 2026
e1f00d4
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 14, 2026
21c0c79
test: fix unit test coverage for google-cloud-bigquery
shuoweil Jul 14, 2026
eb478b9
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 15, 2026
814400e
fix(bigquery): resolve flake8 lint errors in unit tests
shuoweil Jul 15, 2026
ac785fd
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 16, 2026
7bbefa0
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 16, 2026
eac0470
fix(bigquery): prevent pandas-gbq version helper state leak
shuoweil Jul 16, 2026
e8f6c87
style: apply ruff formatting and fix version helper caching leak
shuoweil Jul 16, 2026
60756aa
style: apply nox format across google-cloud-bigquery
shuoweil Jul 16, 2026
906e470
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 16, 2026
fa4537b
fix(test): use InteractiveShell to prevent pytest stdin capture error
shuoweil Jul 16, 2026
db0c1b9
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 20, 2026
d4bd53d
Merge branch 'main' into shuowei-gbq-delegation-telemetry-mvp
shuoweil Jul 21, 2026
c11df77
feat: implement pandas-gbq telemetry and version checks
shuoweil Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
from typing import Any

import packaging.version

from google.cloud.bigquery import exceptions


_MIN_PYARROW_VERSION = packaging.version.Version("3.0.0")
_MIN_BQ_STORAGE_VERSION = packaging.version.Version("2.0.0")
_BQ_STORAGE_OPTIONAL_READ_SESSION_VERSION = packaging.version.Version("2.6.0")
Expand Down Expand Up @@ -247,3 +245,49 @@ def try_import(self, raise_if_error: bool = False) -> Any:
and PYARROW_VERSIONS.try_import() is not None
and PYARROW_VERSIONS.installed_version >= _MIN_PYARROW_VERSION_RANGE
)


class PandasGBQVersions:
"""Version and delegation comparisons for pandas-gbq package."""

def __init__(self):
self._installed_version = None
self._delegation_api_version = None

@property
def installed_version(self) -> packaging.version.Version:
"""Return the parsed version of pandas-gbq"""
if self._installed_version is None:
try:
import pandas_gbq # type: ignore

self._installed_version = packaging.version.parse(
getattr(pandas_gbq, "__version__", "0.0.0")
)
except ImportError:
self._installed_version = packaging.version.parse("0.0.0")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching only ImportError when importing pandas_gbq can lead to unhandled exceptions if the import fails due to other issues (e.g., compiled extension load failures, AttributeError, or TypeError from broken dependencies). Additionally, packaging.version.parse can raise InvalidVersion if the version string is malformed. Catching Exception ensures a robust fallback to version 0.0.0.

Suggested change
if self._installed_version is None:
try:
import pandas_gbq # type: ignore
self._installed_version = packaging.version.parse(
getattr(pandas_gbq, "__version__", "0.0.0")
)
except ImportError:
self._installed_version = packaging.version.parse("0.0.0")
if self._installed_version is None:
try:
import pandas_gbq # type: ignore
self._installed_version = packaging.version.parse(
getattr(pandas_gbq, "__version__", "0.0.0")
)
except Exception:
self._installed_version = packaging.version.parse("0.0.0")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not want to hide errors for users.


return self._installed_version

@property
def delegation_api_version(self) -> int:
"""Return the delegation API version of pandas-gbq if installed, otherwise 0."""
if self._delegation_api_version is None:
try:
import pandas_gbq # type: ignore

self._delegation_api_version = getattr(
pandas_gbq, "_internal_delegation_api_version", 0
)
except ImportError:
self._delegation_api_version = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to installed_version, catching only ImportError when importing pandas_gbq can lead to unhandled exceptions if the import fails due to other issues. Catching Exception ensures a robust fallback to 0.

Suggested change
if self._delegation_api_version is None:
try:
import pandas_gbq # type: ignore
self._delegation_api_version = getattr(
pandas_gbq, "_internal_delegation_api_version", 0
)
except ImportError:
self._delegation_api_version = 0
if self._delegation_api_version is None:
try:
import pandas_gbq # type: ignore
self._delegation_api_version = getattr(
pandas_gbq, "_internal_delegation_api_version", 0
)
except Exception:
self._delegation_api_version = 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not want to hide errors for users.


return self._delegation_api_version

@property
def is_delegation_supported(self) -> bool:
"""True if the installed pandas-gbq version supports query delegation API (version >= 1)."""
return self.delegation_api_version >= 1


PANDAS_GBQ_VERSIONS = PandasGBQVersions()
69 changes: 55 additions & 14 deletions packages/google-cloud-bigquery/google/cloud/bigquery/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
import functools
import operator
import typing
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union, Sequence

import warnings
from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Union

try:
import pandas # type: ignore
Expand Down Expand Up @@ -56,30 +55,33 @@
_read_wkt = wkt.loads

import google.api_core.exceptions
from google.api_core.page_iterator import HTTPIterator

import google.cloud._helpers # type: ignore
from google.cloud.bigquery import _helpers
from google.cloud.bigquery import _pandas_helpers
from google.cloud.bigquery import _versions_helpers
from google.api_core.page_iterator import HTTPIterator
from google.cloud.bigquery import (
_helpers,
_pandas_helpers,
_string_references,
_versions_helpers,
external_config,
)
from google.cloud.bigquery import exceptions as bq_exceptions
from google.cloud.bigquery import schema as _schema
from google.cloud.bigquery._tqdm_helpers import get_progress_bar
from google.cloud.bigquery.encryption_configuration import EncryptionConfiguration
from google.cloud.bigquery.enums import DefaultPandasDTypes
from google.cloud.bigquery.external_config import ExternalConfig
from google.cloud.bigquery import schema as _schema
from google.cloud.bigquery.schema import _build_schema_resource
from google.cloud.bigquery.schema import _parse_schema_resource
from google.cloud.bigquery.schema import _to_schema_fields
from google.cloud.bigquery import external_config
from google.cloud.bigquery import _string_references
from google.cloud.bigquery.schema import (
_build_schema_resource,
_parse_schema_resource,
_to_schema_fields,
)

if typing.TYPE_CHECKING: # pragma: NO COVER
# Unconditionally import optional dependencies again to tell pytype that
# they are not None, avoiding false "no attribute" errors.
import geopandas # type: ignore
import pandas
import pyarrow
import geopandas # type: ignore
from google.cloud import bigquery_storage # type: ignore
from google.cloud.bigquery.dataset import DatasetReference

Expand Down Expand Up @@ -2709,6 +2711,14 @@ def to_dataframe(
is not supported dtype.

"""
if not _versions_helpers.PANDAS_GBQ_VERSIONS.is_delegation_supported:
warnings.warn(
"Retrieving dataframes via the core client is deprecated. "
"Please install 'pandas-gbq' for the new high-performance backend.",
PendingDeprecationWarning,
stacklevel=2,
)

_pandas_helpers.verify_pandas_imports()

if geography_as_object and shapely is None:
Expand Down Expand Up @@ -2801,6 +2811,37 @@ def to_dataframe(
create_bqstorage_client = False
bqstorage_client = None

if _versions_helpers.PANDAS_GBQ_VERSIONS.is_delegation_supported:
import pandas_gbq # type: ignore

if self.client and hasattr(self.client, "_connection") and hasattr(self.client._connection, "_client_info"):
client_info = self.client._connection._client_info
if client_info:
ua = client_info.user_agent or ""
if "pandas-gbq" not in ua:
client_info.user_agent = f"{ua} pandas-gbq/{pandas_gbq.__version__}".strip()
Comment thread
shuoweil marked this conversation as resolved.
Outdated

return pandas_gbq.pandas.from_row_iterator(
self,
bqstorage_client=bqstorage_client,
dtypes=dtypes,
progress_bar_type=progress_bar_type,
create_bqstorage_client=create_bqstorage_client,
geography_as_object=geography_as_object,
bool_dtype=bool_dtype,
int_dtype=int_dtype,
float_dtype=float_dtype,
string_dtype=string_dtype,
date_dtype=date_dtype,
datetime_dtype=datetime_dtype,
time_dtype=time_dtype,
timestamp_dtype=timestamp_dtype,
range_date_dtype=range_date_dtype,
range_datetime_dtype=range_datetime_dtype,
range_timestamp_dtype=range_timestamp_dtype,
timeout=timeout,
)

record_batch = self.to_arrow(
progress_bar_type=progress_bar_type,
bqstorage_client=bqstorage_client,
Expand Down
Loading
Loading