Skip to content

Commit d172408

Browse files
authored
feat(bigquery): support queryResultsFormat and compressionCodec in query_and_wait (#18027)
### Summary of Changes Adds support for fetching query results in Apache Arrow format directly via `query_and_wait()` using `queryResultsFormat="ARROW"` and optional buffer compression (e.g., `compression_codec="LZ4_FRAME"`). 1. **`query_and_wait` & `_job_helpers` Enhancements**: - Added `query_results_format` and `compression_codec` parameters (with `[Beta]` docstring annotations) to `client.query_and_wait()`, `client._query_and_wait_bigframes()`, and `_job_helpers.query_and_wait()`. - Included `queryResultsFormat` in `_job_helpers.keys_allowlist` and populated `formatOptions.arrowSerializationOptions.bufferCompression` in `jobs.query` REST API request payloads. - Refactored `_wait_or_cancel()` to accept and preserve `query_results_format` on returned `RowIterator` instances. 2. **Arrow Serialization & Direct Job Stream Reading**: - Added `RowIterator._download_arrow_from_job_id()` to stream Arrow record batches directly from `projects/{project}/locations/{location}/jobs/{job_id}/streams/_default` via the BigQuery Storage Read API. - Added logic to decode base64 inline `arrowSchema` and `arrowRecordBatch` from the initial `jobs.query` REST response (`_first_page_response`), calculate the starting row `offset`, and resume `read_rows(stream_name, offset=offset)`. - Added an optimization to skip calling `read_rows()` or initializing `BigQueryReadClient` if `jobComplete = True` and all rows were returned within the first page response. 3. **Safety & Enforcement**: - Overrode `pages`, `__iter__`, and `__next__` on `RowIterator` and `_EmptyRowIterator` to raise a descriptive `ValueError` if non-Arrow iteration is attempted when `queryResultsFormat="ARROW"`. 4. **Testing**: - Added comprehensive unit test suite in `tests/unit/test_query_results_format_arrow.py` (16 passing tests) covering request body formatting, parameter propagation, base64 payload decoding, offset calculation, stream URI construction, and Storage client skipping when all rows are present in the first page. --- Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/google-cloud-python/issues) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary)
1 parent 9634907 commit d172408

8 files changed

Lines changed: 855 additions & 7 deletions

File tree

packages/google-cloud-bigquery/google/cloud/bigquery/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
from google.cloud.bigquery.external_config import HivePartitioningOptions
6060
from google.cloud.bigquery.format_options import AvroOptions
6161
from google.cloud.bigquery.format_options import ParquetOptions
62+
from google.cloud.bigquery.enums import QueryResultsCompressionCodec
63+
from google.cloud.bigquery.enums import QueryResultsFormat
6264
from google.cloud.bigquery.job.base import SessionInfo
6365
from google.cloud.bigquery.job import Compression
6466
from google.cloud.bigquery.job import CopyJob
@@ -221,6 +223,8 @@
221223
"KeyResultStatementKind",
222224
"OperationType",
223225
"QueryPriority",
226+
"QueryResultsCompressionCodec",
227+
"QueryResultsFormat",
224228
"RoutineType",
225229
"SchemaUpdateOption",
226230
"SourceFormat",

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,8 @@ def query_and_wait(
430430
job_retry: Optional[retries.Retry],
431431
page_size: Optional[int] = None,
432432
max_results: Optional[int] = None,
433+
query_results_format: Optional[str] = None,
434+
compression_codec: Optional[str] = None,
433435
callback: Callable = lambda _: None,
434436
) -> table.RowIterator:
435437
"""Run the query, wait for it to finish, and return the results.
@@ -475,6 +477,10 @@ def query_and_wait(
475477
request. Non-positive values are ignored.
476478
max_results (Optional[int]):
477479
The maximum total number of rows from this request.
480+
query_results_format (Optional[Union[str, google.cloud.bigquery.enums.QueryResultsFormat]]):
481+
[Beta] The format for query results (e.g. "ARROW" or :class:`~google.cloud.bigquery.enums.QueryResultsFormat.ARROW`).
482+
compression_codec (Optional[Union[str, google.cloud.bigquery.enums.QueryResultsCompressionCodec]]):
483+
[Beta] Compression codec for Arrow serialization (e.g. "LZ4_FRAME" or :class:`~google.cloud.bigquery.enums.QueryResultsCompressionCodec.LZ4_FRAME`).
478484
callback (Callable):
479485
A callback function used by bigframes to report query progress.
480486
@@ -499,6 +505,13 @@ def query_and_wait(
499505
request_body = _to_query_request(
500506
query=query, job_config=job_config, location=location, timeout=api_timeout
501507
)
508+
if query_results_format is not None:
509+
request_body["queryResultsFormat"] = query_results_format
510+
if compression_codec is not None:
511+
request_body.setdefault("formatOptions", {})
512+
request_body["formatOptions"]["arrowSerializationOptions"] = {
513+
"bufferCompression": compression_codec
514+
}
502515

503516
# Some API parameters aren't supported by the jobs.query API. In these
504517
# cases, fallback to a jobs.insert call.
@@ -522,6 +535,7 @@ def query_and_wait(
522535
retry=retry,
523536
page_size=page_size,
524537
max_results=max_results,
538+
query_results_format=query_results_format,
525539
callback=callback,
526540
)
527541

@@ -594,6 +608,7 @@ def do_query():
594608
retry=retry,
595609
page_size=page_size,
596610
max_results=max_results,
611+
query_results_format=query_results_format,
597612
callback=callback,
598613
)
599614

@@ -633,6 +648,7 @@ def do_query():
633648
created=query_results.created,
634649
started=query_results.started,
635650
ended=query_results.ended,
651+
query_results_format=query_results_format,
636652
)
637653

638654
if job_retry is not None:
@@ -673,6 +689,7 @@ def _supported_by_jobs_query(request_body: Dict[str, Any]) -> bool:
673689
"jobTimeoutMs",
674690
"reservation",
675691
"maxSlots",
692+
"queryResultsFormat",
676693
}
677694

678695
unsupported_keys = request_keys - keys_allowlist
@@ -687,6 +704,7 @@ def _wait_or_cancel(
687704
page_size: Optional[int],
688705
max_results: Optional[int],
689706
*,
707+
query_results_format: Optional[str] = None,
690708
callback: Callable = lambda _: None,
691709
) -> table.RowIterator:
692710
"""Wait for a job to complete and return the results.
@@ -731,6 +749,7 @@ def _wait_or_cancel(
731749
ended=job.ended,
732750
)
733751
)
752+
query_results._query_results_format = query_results_format
734753
return query_results
735754
except Exception:
736755
# Attempt to cancel the job since we can't return the results.

packages/google-cloud-bigquery/google/cloud/bigquery/client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3665,6 +3665,8 @@ def query_and_wait(
36653665
job_retry: retries.Retry = DEFAULT_JOB_RETRY,
36663666
page_size: Optional[int] = None,
36673667
max_results: Optional[int] = None,
3668+
query_results_format: Optional[str] = None,
3669+
compression_codec: Optional[str] = None,
36683670
) -> RowIterator:
36693671
"""Run the query, wait for it to finish, and return the results.
36703672
@@ -3712,6 +3714,10 @@ def query_and_wait(
37123714
by this parameter.
37133715
max_results (Optional[int]):
37143716
The maximum total number of rows from this request.
3717+
query_results_format (Optional[Union[str, google.cloud.bigquery.enums.QueryResultsFormat]]):
3718+
[Beta] The format for query results (e.g. "ARROW" or :class:`~google.cloud.bigquery.enums.QueryResultsFormat.ARROW`).
3719+
compression_codec (Optional[Union[str, google.cloud.bigquery.enums.QueryResultsCompressionCodec]]):
3720+
[Beta] Compression codec for Arrow serialization (e.g. "LZ4_FRAME" or :class:`~google.cloud.bigquery.enums.QueryResultsCompressionCodec.LZ4_FRAME`).
37153721
37163722
Returns:
37173723
google.cloud.bigquery.table.RowIterator:
@@ -3742,6 +3748,8 @@ def query_and_wait(
37423748
job_retry=job_retry,
37433749
page_size=page_size,
37443750
max_results=max_results,
3751+
query_results_format=query_results_format,
3752+
compression_codec=compression_codec,
37453753
)
37463754

37473755
def _query_and_wait_bigframes(
@@ -3757,6 +3765,8 @@ def _query_and_wait_bigframes(
37573765
job_retry: retries.Retry = DEFAULT_JOB_RETRY,
37583766
page_size: Optional[int] = None,
37593767
max_results: Optional[int] = None,
3768+
query_results_format: Optional[str] = None,
3769+
compression_codec: Optional[str] = None,
37603770
callback: Callable = lambda _: None,
37613771
) -> RowIterator:
37623772
"""See query_and_wait.
@@ -3789,6 +3799,8 @@ def _query_and_wait_bigframes(
37893799
job_retry=job_retry,
37903800
page_size=page_size,
37913801
max_results=max_results,
3802+
query_results_format=query_results_format,
3803+
compression_codec=compression_codec,
37923804
callback=callback,
37933805
)
37943806

packages/google-cloud-bigquery/google/cloud/bigquery/enums.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,20 @@ class TimestampPrecision(enum.Enum):
495495
"""
496496
For TIMESTAMP type with picosecond precision.
497497
"""
498+
499+
500+
class QueryResultsFormat(str, enum.Enum):
501+
"""[Beta] Format for query results response."""
502+
503+
ARROW = "ARROW"
504+
"""Specifies Apache Arrow format for query results."""
505+
506+
507+
class QueryResultsCompressionCodec(str, enum.Enum):
508+
"""[Beta] Compression codec for Arrow query results serialization."""
509+
510+
LZ4_FRAME = "LZ4_FRAME"
511+
"""Specifies LZ4_FRAME compression codec."""
512+
513+
ZSTD = "ZSTD"
514+
"""Specifies ZSTD compression codec."""

0 commit comments

Comments
 (0)