Skip to content

fix(bigquery): send the same Arrow options on jobs.query and ReadRows - #18646

Open
ohmayr wants to merge 1 commit into
feat/bigquery-arrow-validate-queryfrom
fix/bigquery-arrow-serialization-options
Open

ohmayr wants to merge 1 commit into
feat/bigquery-arrow-validate-queryfrom
fix/bigquery-arrow-serialization-options

Conversation

@ohmayr

@ohmayr ohmayr commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Sends the same arrowSerializationOptions on jobs.query and ReadRows, so the first page and the stream use the same Arrow encoding. Previously compression_codec was sent in formatOptions, which jobs.query ignores, and never reached ReadRows.

  • Sends arrowSerializationOptions at the top level of jobs.query, with explicit defaults for anything the caller didn't set.
  • Passes the same options to read_rows() (feat(bigquery-storage): add Arrow options and cancel to read_rows #18643).
  • Installs the in-repo google-cloud-bigquery-storage in the mypy nox session.
  • Adds unit tests for the request bodies and system tests comparing LZ4_FRAME and ZSTD with uncompressed results.

@ohmayr
ohmayr requested review from a team as code owners October 13, 2026 08:15
@ohmayr
ohmayr requested review from sindhuvy and removed request for a team October 13, 2026 08:15

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces support for passing arrowSerializationOptions (such as compression codecs) from jobs.query down to ReadRows in the BigQuery Storage API when using the ARROW format. This involves updating RowIterator to store these options, modifying query_and_wait to set them, and updating download_arrow_from_job to pass them to read_rows. Feedback was provided to make _read_rows_options more robust against potential KeyErrors by using .get() with default fallbacks instead of direct dictionary lookups.

Comment on lines +118 to +125
def _read_rows_options(options: Optional[Dict[str, str]]) -> Dict[str, str]:
"""Convert ``jobs.query`` ``arrowSerializationOptions`` to the
``ArrowSerializationOptions`` fields that ``ReadRows`` takes."""
options = options or serialization_options(None)
return {
"buffer_compression": options["bufferCompression"],
"picos_timestamp_precision": options["picosTimestampPrecision"],
}

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

Using direct dictionary lookups like options["bufferCompression"] can raise a KeyError if the dictionary is passed with missing keys (for example, if a user instantiates RowIterator with custom options). It is safer and more robust to use .get() with fallback defaults, which also simplifies the fallback logic when options is None.

Suggested change
def _read_rows_options(options: Optional[Dict[str, str]]) -> Dict[str, str]:
"""Convert ``jobs.query`` ``arrowSerializationOptions`` to the
``ArrowSerializationOptions`` fields that ``ReadRows`` takes."""
options = options or serialization_options(None)
return {
"buffer_compression": options["bufferCompression"],
"picos_timestamp_precision": options["picosTimestampPrecision"],
}
def _read_rows_options(options: Optional[Dict[str, str]]) -> Dict[str, str]:
"""Convert jobs.query arrowSerializationOptions to the
ArrowSerializationOptions fields that ReadRows takes."""
options = options or {}
return {
"buffer_compression": options.get("bufferCompression", "COMPRESSION_UNSPECIFIED"),
"picos_timestamp_precision": options.get("picosTimestampPrecision", "TIMESTAMP_PRECISION_MICROS"),
}

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.

These options always come from serialization_options(), which sets both keys, so a missing key would be a bug I'd want to see as a KeyError rather than paper over with defaults. RowIterator's constructor is internal to query_and_wait() here.

query_and_wait() sent compression_codec inside formatOptions, which
only has useInt64Timestamp and timestampOutputFormat, so jobs.query
ignored it, and ReadRows never got it at all. The first page and the
stream each used their own endpoint's defaults, so one result could
mix encodings.

Build one arrowSerializationOptions value for ARROW queries and send
it at the top level of the jobs.query request, where it also joins
the jobs.query allowlist. Options the caller didn't set are sent with
their defaults (COMPRESSION_UNSPECIFIED, TIMESTAMP_PRECISION_MICROS),
so the encoding never depends on server defaults. RowIterator keeps
the value on both the jobs.query and the waited-job paths and sends
it on read_rows(). jobs.insert never gets these fields, since they
come from query_and_wait() arguments rather than QueryJobConfig.

The mypy session now installs the in-repo google-cloud-bigquery-storage
too, so the new read_rows() argument is type-checked.
@ohmayr
ohmayr force-pushed the fix/bigquery-arrow-serialization-options branch from a8b7900 to 6543176 Compare October 13, 2026 10:02
@ohmayr
ohmayr requested a review from tswast October 13, 2026 20:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant