Skip to content

feat(bigquery): add ArrowQueryResult and ArrowQueryResultImpl for Arrow result streaming - #13944

Merged
jinseopkim0 merged 38 commits into
mainfrom
feat-bigquery-arrow-veneer
Sep 16, 2026
Merged

jinseopkim0 merged 38 commits into
mainfrom
feat-bigquery-arrow-veneer

Conversation

@jinseopkim0

@jinseopkim0 jinseopkim0 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the ArrowQueryResult interface and ArrowQueryResultImpl class for streaming and paginating query results backed by Apache Arrow record batches.

It provides zero-copy VectorSchemaRoot streaming and implements the standard TableResult iteration contract over Arrow batches.

@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 Arrow-formatted query results in the BigQuery client, adding ArrowQueryPageFetcher to fetch Arrow pages and updating response parsing to handle Arrow schemas and record batches. The review feedback highlights critical issues that need to be addressed: a potential NotSerializableException in ArrowQueryPageFetcher due to a non-serializable schema field, memory leaks from unclosed Arrow vectors and record batches, and overly restrictive type checks (instanceof List instead of instanceof Collection) when determining page row counts.

@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch 3 times, most recently from 2f4435e to 76600f0 Compare August 7, 2026 19:40
@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch from 76600f0 to ec26676 Compare August 7, 2026 19:51
@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch from ec26676 to 68798eb Compare August 7, 2026 20:13
@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch 2 times, most recently from bb63672 to 88530f7 Compare August 7, 2026 20:32
@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch from 88530f7 to 80ae0af Compare August 10, 2026 15:25
@jinseopkim0

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@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 the Apache Arrow query results format in the BigQuery client. It refactors Arrow schema conversion and vector creation logic into a new helper class ArrowPojoUtils, adds an ArrowQueryPageFetcher to stream Arrow-formatted query results, and updates BigQueryImpl and QueryRequestInfo to handle Arrow serialization options and deserialize Arrow record batches. The review feedback highlights several improvement opportunities: preventing a potential resource leak in ArrowPojoUtils.createVectors by closing already allocated vectors if an exception is thrown, avoiding a potential NullPointerException in BigQueryImpl by using serviceOptions.getCredentialsProvider(), and extracting duplicate logic for computing firstPageRows to improve maintainability.

@jinseopkim0

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@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 the Apache Arrow query results format in the BigQuery client. It extracts Arrow conversion utilities into a new ArrowPojoUtils helper class, implements ArrowQueryPageFetcher to stream results via the BigQuery Storage Read API, and updates query execution paths to handle Arrow schemas and record batches. The review feedback focuses on critical resource management and performance optimizations, specifically ensuring exception-safe LIFO cleanup of Arrow vectors, allocators, and schema roots to prevent memory leaks, reusing allocator and root instances across page fetches, enforcing the maxResults limit, and properly handling credentials and universe domains.

@jinseopkim0

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@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 retrieving query results in Apache Arrow format using the fast query path and the BigQuery Storage API. It adds a new ArrowQueryPageFetcher to handle paginated Arrow record batches, introduces ArrowPojoUtils for Arrow schema and vector utilities, and updates BigQueryImpl to deserialize Arrow schemas and record batches. The review feedback highlights a critical resource leak in ArrowQueryPageFetcher due to its stateful nature across pages, suggesting a stateless implementation using try-with-resources to ensure gRPC streams, clients, and off-heap memory allocators are safely closed.

@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch from 8f1d641 to 101d2d0 Compare August 12, 2026 14:47
@jinseopkim0

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@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 the Arrow query results format in BigQuery, extracting Arrow conversion utilities into ArrowPojoUtils and implementing ArrowQueryPageFetcher in BigQueryImpl to read Arrow rows via the Storage Read API. The reviewer identifies a critical issue where direct imports and references to Apache Arrow classes in BigQueryImpl.java will cause NoClassDefFoundError at runtime for users who do not have Arrow on their classpath. To resolve this, the reviewer recommends encapsulating all Arrow-specific logic and deserialization within ArrowDeserializer and refactoring BigQueryImpl to avoid direct bytecode dependencies on optional Arrow classes.

@jinseopkim0

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@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 the Apache Arrow query results format in the BigQuery client, enabling faster query path execution. Key changes include the addition of ArrowPojoUtils for schema conversions, updates to ArrowDeserializer to handle row loading and deserialization, and the implementation of ArrowQueryPageFetcher in BigQueryImpl to fetch pages of Arrow-formatted results. The review feedback highlights three improvement opportunities: caching the BigQueryReadClient and stream iterator in transient fields within ArrowQueryPageFetcher to prevent significant gRPC overhead on pagination, removing an unused BufferAllocator in ArrowDeserializer.deserializeSchema, and restructuring vector resource management in loadArrowRows to avoid a potential double-closing issue.

@jinseopkim0

Copy link
Copy Markdown
Contributor Author

@gemini-code-assist review

@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 retrieving query results in Apache Arrow format from BigQuery. It adds the ArrowDeserializer and ArrowPojoUtils helper classes to convert Arrow schemas and deserialize record batches, implements ArrowQueryPageFetcher to fetch pages of Arrow rows, and integrates these changes into BigQueryImpl and QueryRequestInfo. Feedback on these changes highlights several critical issues: potential resource leaks if the stream iterator is not fully exhausted, a risk of generating invalid stream names if the job location is null, high memory consumption due to a hardcoded page size of 100,000 rows, performance overhead from repeatedly instantiating RootAllocator, and reduced type safety from using Object instead of concrete Arrow types in method signatures.

@jinseopkim0
jinseopkim0 force-pushed the feat-bigquery-arrow-veneer branch from 10b3e9c to 1b283db Compare September 14, 2026 20:53
@jinseopkim0 jinseopkim0 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 14, 2026
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 14, 2026
@jinseopkim0
jinseopkim0 requested a review from lqiu96 September 15, 2026 20:50
* underlying gRPC streaming channels are deterministically released.
*/
@BetaApi
public interface ArrowQueryResult extends AutoCloseable, Iterable<VectorSchemaRoot> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: perhaps we should add the javadoc that this represents the canonical BigQuery TableResult

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.

Thanks for the comment, updated.

Comment on lines +374 to +376
if (!yieldedInitialBatch
&& initialRecordBatchBytes != null
&& initialRecordBatchBytes.length > 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we move this to a helper function explain the logic in here. I also see this being used elsewhere as well

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.

Thanks for the comment, done.

Comment on lines +425 to +426
} catch (NoSuchElementException | BigQueryException e) {
throw e;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, can you explain why these two exceptions specifically?

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.

Thanks for the question. These two exceptions are rethrown directly to avoid improper wrapping:

  1. NoSuchElementException: Required by the standard Java Iterator.next() contract when no more elements remain. Rethrowing directly prevents it from falling through to catch (Exception e) and getting wrapped into a BigQueryException.
  2. BigQueryException: Avoids redundant double-wrapping (BigQueryException wrapping another BigQueryException), preserving the original error code, message, and root cause.

Any other unexpected exception caught in the subsequent catch (Exception e) block is wrapped in a BigQueryException to ensure uniform error handling in the veneer.

I added a comment to clarify.

Comment on lines +428 to +430
if (isClosed()) {
throw new NoSuchElementException("Query stream was closed.");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, why we do need to throw an exception here?

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.

Thanks for the question. When close() is called, it cancels the active gRPC stream. Catching this and checking if (isClosed()) ensures we throw NoSuchElementException (fulfilling the Java Iterator contract) rather than misreporting an intentional cancellation as an unexpected network failure (BigQueryException).

@@ -0,0 +1 @@
mock-maker-inline

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, what is this needed for?

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.

This enables Mockito's inline mock maker, which allows mocking final classes (such as the auto-generated BigQueryReadClient and GAX ServerStream) in unit tests (ArrowQueryResultTest) without having to spin up an in-memory gRPC server.

Comment on lines +494 to +505
loadBatch(new ByteArrayReadableSeekableByteChannel(bytes));
}

/**
* Deserializes an Arrow record batch from a protobuf {@link ByteString} and loads it into the
* root vector.
*
* @param byteString serialized Arrow record batch bytes as a {@link ByteString}
* @throws IOException if deserialization fails
*/
private void loadBatch(ByteString byteString) throws IOException {
loadBatch(Channels.newChannel(byteString.newInput()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do the channels created in here need to be closed?

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.

Thanks for the feedback. I've updated the code accordingly.

static ArrowQueryResultImpl fromReadSession(
ReadSession readSession, JobId jobId, BigQueryReadClient readClient) {
Schema pojoSchema = null;
if (readSession.hasArrowSchema()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, what happens if there is no arrow schema?

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.

If readSession has no Arrow schema:

  1. If streams are present: The constructor throws an IllegalArgumentException ("Arrow schema cannot be null when query results or streams are present.") since records cannot be deserialized without a schema.
  2. If no streams are present (e.g. 0 rows / empty result): arrowSchema defaults to an empty Schema (new Schema(Collections.emptyList())), maintaining non-null internal state and yielding an empty iterator (0 batches).

@lqiu96 lqiu96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added some small comments.

@jinseopkim0 jinseopkim0 added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 16, 2026
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 16, 2026
@jinseopkim0
jinseopkim0 merged commit a62fdf8 into main Sep 16, 2026
209 of 210 checks passed
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.

3 participants