Skip to content

Commit fc79738

Browse files
committed
fix(bigquery): keep the Arrow first page and wait without rows
When an ARROW query's results didn't fit in the jobs.query response, QueryJob.result() dropped that response, because it only kept first pages that had STRUCT_ENCODING rows. The stream then reread every row from offset 0. While waiting for the job, query_and_wait() also passed page_size to jobs.getQueryResults, which returned STRUCT_ENCODING rows that ARROW results never use. For ARROW queries, decide whether rows remain by comparing pageRowCount with totalRows instead of checking for a pageToken, wait with page_size=0 so jobs.getQueryResults is called with maxResults=0, and keep a jobs.query response that has arrowRecordBatch as the first page. RowIterator then returns the first page and streams the rest from the next row, also when it's read again. Queries that don't use ARROW are unchanged.
1 parent aa55288 commit fc79738

7 files changed

Lines changed: 482 additions & 9 deletions

File tree

‎packages/google-cloud-bigquery/google/cloud/bigquery/_arrow_query.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from __future__ import annotations
1818

1919
import base64
20-
from typing import Dict, Iterator, Optional, TYPE_CHECKING
20+
from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING
2121

2222
from google.cloud.bigquery import _versions_helpers
2323
from google.cloud.bigquery import enums
@@ -113,6 +113,16 @@ def serialization_options(
113113
}
114114

115115

116+
def has_more_rows(response: Dict[str, Any]) -> bool:
117+
"""Return True if rows remain after an ``ARROW`` ``jobs.query`` response.
118+
119+
``ARROW`` responses say how many rows the first page holds in
120+
``pageRowCount``, so compare that with ``totalRows`` instead of checking
121+
for a ``pageToken``.
122+
"""
123+
return int(response.get("pageRowCount", 0)) < int(response.get("totalRows", 0))
124+
125+
116126
def _read_rows_options(options: Optional[Dict[str, str]]) -> Dict[str, str]:
117127
"""Convert ``jobs.query`` ``arrowSerializationOptions`` to the
118128
``ArrowSerializationOptions`` fields that ``ReadRows`` takes."""
@@ -153,7 +163,6 @@ def download_arrow_from_job(
153163
# Step 2: Process initial inline Arrow response from jobs.query if available.
154164
if row_iterator._first_page_response:
155165
first_page = row_iterator._first_page_response
156-
row_iterator._first_page_response = None
157166

158167
job_complete = bool(first_page.get("jobComplete", False))
159168
if job_complete:

‎packages/google-cloud-bigquery/google/cloud/bigquery/_job_helpers.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,10 @@ def query_and_wait(
533533
)
534534
request_body["arrowSerializationOptions"] = arrow_serialization_options
535535

536+
# jobs.getQueryResults only returns STRUCT_ENCODING rows, so while waiting
537+
# for an ARROW query, request none of them.
538+
wait_page_size = 0 if arrow_serialization_options is not None else page_size
539+
536540
# Some API parameters aren't supported by the jobs.query API. In these
537541
# cases, fallback to a jobs.insert call.
538542
if not _supported_by_jobs_query(request_body):
@@ -553,7 +557,7 @@ def query_and_wait(
553557
api_timeout=api_timeout,
554558
wait_timeout=wait_timeout,
555559
retry=retry,
556-
page_size=page_size,
560+
page_size=wait_page_size,
557561
max_results=max_results,
558562
query_results_format=query_results_format,
559563
arrow_serialization_options=arrow_serialization_options,
@@ -616,6 +620,8 @@ def do_query():
616620
)
617621
page_token = query_results.page_token
618622
more_pages = page_token is not None
623+
if arrow_serialization_options is not None:
624+
more_pages = _arrow_query.has_more_rows(response)
619625

620626
if more_pages or not query_results.complete:
621627
# TODO(swast): Avoid a call to jobs.get in some cases (few
@@ -627,7 +633,7 @@ def do_query():
627633
api_timeout=api_timeout,
628634
wait_timeout=wait_timeout,
629635
retry=retry,
630-
page_size=page_size,
636+
page_size=wait_page_size,
631637
max_results=max_results,
632638
query_results_format=query_results_format,
633639
arrow_serialization_options=arrow_serialization_options,

‎packages/google-cloud-bigquery/google/cloud/bigquery/job/query.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,9 +1842,13 @@ def is_job_done():
18421842
# RowIterator response if there are any rows in it. This prevents us
18431843
# from stopping the iteration early in the cases where we set
18441844
# maxResults=0. In that case, we're missing rows and there's no next
1845-
# page token.
1845+
# page token. ARROW responses from jobs.query carry their rows in
1846+
# arrowRecordBatch instead of rows.
18461847
first_page_response = self._query_results._properties
1847-
if "rows" not in first_page_response:
1848+
if (
1849+
"rows" not in first_page_response
1850+
and "arrowRecordBatch" not in first_page_response
1851+
):
18481852
first_page_response = None
18491853

18501854
rows = self._client._list_rows_from_query_results(

0 commit comments

Comments
 (0)