Skip to content

Commit ff58ec9

Browse files
authored
test: parametrize to_arrow empty stream tests and update docstrings (#17919)
## Problem When reading from an empty BigQuery table using the Read API, the stream may return no messages. Previously, this could lead to an `AttributeError` when trying to parse the schema from a `None` parser. While a fallback was added to return an empty table, this fallback uses an empty schema (zero columns), which can cause downstream consumers (like Vertex Ray) to fail if they expect specific columns to be present. ## Solution 1. **Updated Documentation**: Added explanatory comments and updated docstrings in `reader.py` to clarify the flow between `ReadRowsStream.to_arrow()` and `ReadRowsIterable.to_arrow()`, and to inform users about the fallback behavior. 2. **Added Parametrized Tests**: Added comprehensive tests to `test_reader_v1_arrow.py` to verify the behavior of `to_arrow()` on empty streams, both with and without a `read_session` provided. ## Notes to Reviewers - This PR does not change functionality but adds testing and documentation around existing behavior to prevent future regression and aid debugging. The functionality that should ensure the correct behavior in internal bug #352600521 was added in February in [PR # 15575](#15575). This PR is less about fixing the bug versus documenting the correct use of the code to avoid issues. - To guarantee the correct schema (non-empty) for empty streams, callers should provide the `read_session` to `ReadRowsStream.to_arrow()`. Fixes internal bug #352600521
1 parent 4af69c4 commit ff58ec9

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

packages/google-cloud-bigquery-storage/google/cloud/bigquery_storage_v1/reader.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,13 @@ def __iter__(self):
349349
def to_arrow(self):
350350
"""Create a :class:`pyarrow.Table` of all rows in the stream.
351351
352+
Note: This is the :class:`ReadRowsIterable` version of ``to_arrow``. It is
353+
typically invoked by calling :meth:`ReadRowsStream.to_arrow`, which
354+
delegates here after handling optional session context. The key difference
355+
is that :meth:`ReadRowsStream.to_arrow` accepts a ``read_session`` argument
356+
to provide schema hints for empty streams, whereas this method relies on
357+
the parser initialized during :class:`ReadRowsIterable` construction.
358+
352359
This method requires the pyarrow library and a stream using the Arrow
353360
format.
354361
@@ -365,6 +372,9 @@ def to_arrow(self):
365372

366373
# No data, return an empty Table.
367374
if self._stream_parser is None:
375+
# Note: This returns a table with an empty schema (no columns).
376+
# Downstream consumers (like Vertex Ray) might fail if they expect specific columns.
377+
# To guarantee the correct schema, provide 'read_session' to `ReadRowsStream.to_arrow()`.
368378
return pyarrow.Table.from_batches([], schema=pyarrow.schema([]))
369379

370380
self._stream_parser._parse_arrow_schema()

packages/google-cloud-bigquery-storage/tests/unit/test_reader_v1_arrow.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import importlib_metadata as metadata
2828

2929
import google.api_core.exceptions
30-
3130
from google.cloud.bigquery_storage import types
3231

3332
from .helpers import SCALAR_BLOCKS, SCALAR_COLUMN_NAMES, SCALAR_COLUMNS
@@ -208,6 +207,32 @@ def test_rows_w_empty_stream_arrow(class_under_test, mock_gapic_client):
208207
assert tuple(got) == ()
209208

210209

210+
@pytest.mark.parametrize(
211+
"use_session",
212+
[False, True],
213+
ids=["no_session", "with_session"],
214+
)
215+
def test_to_arrow_empty_stream(class_under_test, mock_gapic_client, use_session):
216+
"""Verify that to_arrow() handles empty streams safely.
217+
218+
Note: This test focuses specifically on ReadRowsStream.to_arrow(), which
219+
accepts a read_session argument to provide schema hints for empty streams,
220+
unlike ReadRowsIterable.to_arrow().
221+
"""
222+
arrow_schema = _bq_to_arrow_schema(SCALAR_COLUMNS)
223+
mock_gapic_client.read_rows.return_value = iter([])
224+
225+
reader = class_under_test(mock_gapic_client, "name", 0, {})
226+
227+
read_session = _generate_arrow_read_session(arrow_schema) if use_session else None
228+
expected_schema = arrow_schema if use_session else pyarrow.schema([])
229+
230+
table = reader.to_arrow(read_session)
231+
232+
assert len(table) == 0
233+
assert table.schema == expected_schema
234+
235+
211236
def test_rows_w_scalars_arrow(class_under_test, mock_gapic_client):
212237
arrow_schema = _bq_to_arrow_schema(SCALAR_COLUMNS)
213238
arrow_batches = _bq_to_arrow_batches(SCALAR_BLOCKS, arrow_schema)

0 commit comments

Comments
 (0)