Skip to content

Commit 7208b63

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 1a85fb2 commit 7208b63

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
@@ -129,6 +129,16 @@ def serialization_options(
129129
}
130130

131131

132+
def has_more_rows(response: Dict[str, Any]) -> bool:
133+
"""Return True if rows remain after an ``ARROW`` ``jobs.query`` response.
134+
135+
``ARROW`` responses say how many rows the first page holds in
136+
``pageRowCount``, so compare that with ``totalRows`` instead of checking
137+
for a ``pageToken``.
138+
"""
139+
return int(response.get("pageRowCount", 0)) < int(response.get("totalRows", 0))
140+
141+
132142
def _read_rows_options(options: Optional[Dict[str, str]]) -> Dict[str, str]:
133143
"""Convert ``jobs.query`` ``arrowSerializationOptions`` to the
134144
``ArrowSerializationOptions`` fields that ``ReadRows`` takes."""
@@ -169,7 +179,6 @@ def download_arrow_from_job(
169179
# Step 2: Process initial inline Arrow response from jobs.query if available.
170180
if row_iterator._first_page_response:
171181
first_page = row_iterator._first_page_response
172-
row_iterator._first_page_response = None
173182

174183
job_complete = bool(first_page.get("jobComplete", False))
175184
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
@@ -534,6 +534,10 @@ def query_and_wait(
534534
)
535535
request_body["arrowSerializationOptions"] = arrow_serialization_options
536536

537+
# jobs.getQueryResults only returns STRUCT_ENCODING rows, so while waiting
538+
# for an ARROW query, request none of them.
539+
wait_page_size = 0 if arrow_serialization_options is not None else page_size
540+
537541
# Some API parameters aren't supported by the jobs.query API. In these
538542
# cases, fallback to a jobs.insert call.
539543
if not _supported_by_jobs_query(request_body):
@@ -554,7 +558,7 @@ def query_and_wait(
554558
api_timeout=api_timeout,
555559
wait_timeout=wait_timeout,
556560
retry=retry,
557-
page_size=page_size,
561+
page_size=wait_page_size,
558562
max_results=max_results,
559563
query_results_format=query_results_format,
560564
arrow_serialization_options=arrow_serialization_options,
@@ -617,6 +621,8 @@ def do_query():
617621
)
618622
page_token = query_results.page_token
619623
more_pages = page_token is not None
624+
if arrow_serialization_options is not None:
625+
more_pages = _arrow_query.has_more_rows(response)
620626

621627
if more_pages or not query_results.complete:
622628
# TODO(swast): Avoid a call to jobs.get in some cases (few
@@ -628,7 +634,7 @@ def do_query():
628634
api_timeout=api_timeout,
629635
wait_timeout=wait_timeout,
630636
retry=retry,
631-
page_size=page_size,
637+
page_size=wait_page_size,
632638
max_results=max_results,
633639
query_results_format=query_results_format,
634640
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)