2222
2323import com .google .api .core .BetaApi ;
2424import com .google .api .core .InternalApi ;
25+ import com .google .api .gax .core .FixedCredentialsProvider ;
2526import com .google .api .gax .paging .Page ;
27+ import com .google .api .gax .rpc .ServerStream ;
2628import com .google .api .services .bigquery .model .ErrorProto ;
2729import com .google .api .services .bigquery .model .GetQueryResultsResponse ;
2830import com .google .api .services .bigquery .model .ProjectList ;
4345import com .google .cloud .bigquery .InsertAllRequest .RowToInsert ;
4446import com .google .cloud .bigquery .spi .v2 .BigQueryRpc ;
4547import com .google .cloud .bigquery .spi .v2 .HttpBigQueryRpc ;
48+ import com .google .cloud .bigquery .storage .v1 .BigQueryReadClient ;
49+ import com .google .cloud .bigquery .storage .v1 .BigQueryReadSettings ;
50+ import com .google .cloud .bigquery .storage .v1 .ReadRowsRequest ;
51+ import com .google .cloud .bigquery .storage .v1 .ReadRowsResponse ;
4652import com .google .common .annotations .VisibleForTesting ;
4753import com .google .common .base .Function ;
4854import com .google .common .base .Strings ;
5763import io .opentelemetry .context .Scope ;
5864import java .io .IOException ;
5965import java .util .ArrayList ;
66+ import java .util .Iterator ;
6067import java .util .List ;
6168import java .util .Map ;
6269import java .util .concurrent .Callable ;
@@ -264,6 +271,143 @@ public Page<FieldValueList> getNextPage() {
264271 }
265272 }
266273
274+ private static class ArrowQueryPageFetcher implements NextPageFetcher <FieldValueList > {
275+ private static final long serialVersionUID = 1L ;
276+
277+ private final JobId jobId ;
278+ private final Schema schema ;
279+ private final org .apache .arrow .vector .types .pojo .Schema arrowSchemaPojo ;
280+ private final BigQueryOptions serviceOptions ;
281+ private final long maxResults ;
282+
283+ private transient BigQueryReadClient bqReadClient ;
284+ private transient ServerStream <ReadRowsResponse > stream ;
285+ private transient Iterator <ReadRowsResponse > streamIterator ;
286+ private long totalRowsReturned = 0L ;
287+ private boolean streamClosed = false ;
288+
289+ ArrowQueryPageFetcher (
290+ JobId jobId ,
291+ Schema schema ,
292+ org .apache .arrow .vector .types .pojo .Schema arrowSchemaPojo ,
293+ BigQueryOptions serviceOptions ,
294+ long initialRowOffset ,
295+ Long maxResults ) {
296+ this .jobId = jobId ;
297+ this .schema = schema ;
298+ this .arrowSchemaPojo = arrowSchemaPojo ;
299+ this .serviceOptions = serviceOptions ;
300+ this .totalRowsReturned = initialRowOffset ;
301+ this .maxResults = maxResults != null ? maxResults : Long .MAX_VALUE ;
302+ }
303+
304+ @ Override
305+ public Page <FieldValueList > getNextPage () {
306+ if (streamClosed || totalRowsReturned >= maxResults ) {
307+ closeClient ();
308+ return null ;
309+ }
310+
311+ long pageSize = 100000L ;
312+ List <FieldValueList > rowBatch = new ArrayList <>();
313+
314+ try {
315+ if (bqReadClient == null ) {
316+ BigQueryReadSettings settings =
317+ BigQueryReadSettings .newBuilder ()
318+ .setCredentialsProvider (
319+ FixedCredentialsProvider .create (serviceOptions .getCredentials ()))
320+ .build ();
321+ bqReadClient = BigQueryReadClient .create (settings );
322+ }
323+
324+ if (streamIterator == null ) {
325+ String streamName =
326+ String .format (
327+ "projects/%s/locations/%s/jobs/%s/streams/_default" ,
328+ jobId .getProject () != null ? jobId .getProject () : serviceOptions .getProjectId (),
329+ jobId .getLocation () != null ? jobId .getLocation () : serviceOptions .getLocation (),
330+ jobId .getJob ());
331+
332+ ReadRowsRequest readRowsRequest =
333+ ReadRowsRequest .newBuilder ()
334+ .setReadStream (streamName )
335+ .setOffset (totalRowsReturned )
336+ .build ();
337+
338+ stream = bqReadClient .readRowsCallable ().call (readRowsRequest );
339+ streamIterator = stream .iterator ();
340+ }
341+
342+ try (org .apache .arrow .memory .BufferAllocator allocator =
343+ new org .apache .arrow .memory .RootAllocator (Long .MAX_VALUE )) {
344+ List <org .apache .arrow .vector .FieldVector > vectors = new ArrayList <>();
345+ for (org .apache .arrow .vector .types .pojo .Field field : arrowSchemaPojo .getFields ()) {
346+ vectors .add ((org .apache .arrow .vector .FieldVector ) field .createVector (allocator ));
347+ }
348+ try (org .apache .arrow .vector .VectorSchemaRoot root =
349+ new org .apache .arrow .vector .VectorSchemaRoot (vectors )) {
350+ org .apache .arrow .vector .VectorLoader loader =
351+ new org .apache .arrow .vector .VectorLoader (root );
352+
353+ while (rowBatch .size () < pageSize && streamIterator .hasNext ()) {
354+ ReadRowsResponse response = streamIterator .next ();
355+ if (response .hasArrowRecordBatch ()) {
356+ com .google .cloud .bigquery .storage .v1 .ArrowRecordBatch batch =
357+ response .getArrowRecordBatch ();
358+ org .apache .arrow .vector .ipc .message .ArrowRecordBatch deserializedBatch =
359+ org .apache .arrow .vector .ipc .message .MessageSerializer .deserializeRecordBatch (
360+ new org .apache .arrow .vector .ipc .ReadChannel (
361+ new org .apache .arrow .vector .util .ByteArrayReadableSeekableByteChannel (
362+ batch .getSerializedRecordBatch ().toByteArray ())),
363+ allocator );
364+ loader .load (deserializedBatch );
365+ deserializedBatch .close ();
366+ int batchRowCount = root .getRowCount ();
367+ for (int i = 0 ; i < batchRowCount ; i ++) {
368+ rowBatch .add (ArrowDeserializer .arrowRootToFieldValueList (root , i , schema ));
369+ }
370+ root .clear ();
371+ }
372+ }
373+ }
374+ }
375+
376+ if (rowBatch .isEmpty ()) {
377+ streamClosed = true ;
378+ closeClient ();
379+ return null ;
380+ }
381+
382+ totalRowsReturned += rowBatch .size ();
383+
384+ String nextPageToken = null ;
385+ if (streamIterator .hasNext () && totalRowsReturned < maxResults ) {
386+ nextPageToken = String .valueOf (totalRowsReturned );
387+ } else {
388+ streamClosed = true ;
389+ closeClient ();
390+ }
391+
392+ return new PageImpl <>(this , nextPageToken , rowBatch );
393+
394+ } catch (Exception e ) {
395+ streamClosed = true ;
396+ closeClient ();
397+ throw new BigQueryException (0 , "Failed to read Arrow rows from storage stream" , e );
398+ }
399+ }
400+
401+ private void closeClient () {
402+ if (bqReadClient != null ) {
403+ bqReadClient .close ();
404+ bqReadClient = null ;
405+ }
406+ streamIterator = null ;
407+ stream = null ;
408+ }
409+ }
410+
267411 private final HttpBigQueryRpc bigQueryRpc ;
268412
269413 private static final BigQueryRetryConfig EMPTY_RETRY_CONFIG =
@@ -2077,8 +2221,28 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20772221
20782222 long numRows ;
20792223 Schema schema ;
2080- if (results .getJobComplete () && results .getSchema () != null ) {
2081- schema = Schema .fromPb (results .getSchema ());
2224+ boolean isArrow = false ;
2225+ org .apache .arrow .vector .types .pojo .Schema arrowSchemaPojo = null ;
2226+
2227+ if (results .getJobComplete ()) {
2228+ if (results .getSchema () != null ) {
2229+ schema = Schema .fromPb (results .getSchema ());
2230+ } else if (results .getArrowSchema () != null ) {
2231+ isArrow = true ;
2232+ try {
2233+ arrowSchemaPojo =
2234+ org .apache .arrow .vector .ipc .message .MessageSerializer .deserializeSchema (
2235+ new org .apache .arrow .vector .ipc .ReadChannel (
2236+ new org .apache .arrow .vector .util .ByteArrayReadableSeekableByteChannel (
2237+ results .getArrowSchema ().decodeSerializedSchema ())));
2238+ schema = ArrowDeserializer .arrowSchemaToBigQuerySchema (arrowSchemaPojo );
2239+ } catch (IOException e ) {
2240+ throw new BigQueryException (0 , "Failed to deserialize Arrow schema from response" , e );
2241+ }
2242+ } else {
2243+ schema = null ;
2244+ }
2245+
20822246 if (results .getNumDmlAffectedRows () == null && results .getTotalRows () == null ) {
20832247 numRows = 0L ;
20842248 } else if (results .getNumDmlAffectedRows () != null ) {
@@ -2098,42 +2262,91 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20982262 if (results .getPageToken () != null ) {
20992263 JobId jobId = JobId .fromPb (results .getJobReference ());
21002264 String cursor = results .getPageToken ();
2265+
2266+ Iterable <FieldValueList > firstPageRows ;
2267+ NextPageFetcher <FieldValueList > pageFetcher ;
2268+
2269+ if (isArrow ) {
2270+ if (results .getArrowRecordBatch () != null ) {
2271+ try {
2272+ firstPageRows =
2273+ ArrowDeserializer .deserializeRecordBatch (
2274+ results .getArrowRecordBatch ().decodeSerializedRecordBatch (),
2275+ schema ,
2276+ arrowSchemaPojo );
2277+ } catch (IOException e ) {
2278+ throw new BigQueryException (0 , "Failed to deserialize Arrow record batch" , e );
2279+ }
2280+ } else {
2281+ firstPageRows = ImmutableList .of ();
2282+ }
2283+ long initialRowOffset =
2284+ firstPageRows instanceof List ? ((List <?>) firstPageRows ).size () : 0L ;
2285+ pageFetcher =
2286+ new ArrowQueryPageFetcher (
2287+ jobId ,
2288+ schema ,
2289+ arrowSchemaPojo ,
2290+ getOptions (),
2291+ initialRowOffset ,
2292+ null ); // Or use maxResults from configuration if available
2293+ } else {
2294+ firstPageRows =
2295+ transformTableData (
2296+ results .getRows (), schema , getOptions ().getDataFormatOptions ().useInt64Timestamp ());
2297+ pageFetcher = new QueryPageFetcher (jobId , schema , getOptions (), cursor , optionMap (options ));
2298+ }
2299+
21012300 return TableResult .newBuilder ()
21022301 .setSchema (schema )
21032302 .setTotalRows (numRows )
2104- .setPageNoSchema (
2105- new PageImpl <>(
2106- // fetch next pages of results
2107- new QueryPageFetcher (jobId , schema , getOptions (), cursor , optionMap (options )),
2108- cursor ,
2109- transformTableData (
2110- results .getRows (),
2111- schema ,
2112- getOptions ().getDataFormatOptions ().useInt64Timestamp ())))
2303+ .setPageNoSchema (new PageImpl <>(pageFetcher , cursor , firstPageRows ))
21132304 .setJobId (jobId )
21142305 .setQueryId (results .getQueryId ())
21152306 .setJobCreationReason (JobCreationReason .fromPb (results .getJobCreationReason ()))
2116- .setRowsInPage (results .getRows () != null ? (long ) results .getRows ().size () : 0L )
2307+ .setRowsInPage (
2308+ firstPageRows instanceof List ? (long ) ((List <?>) firstPageRows ).size () : 0L )
21172309 .build ();
21182310 }
21192311 // only 1 page of result
2312+ Iterable <FieldValueList > firstPageRows ;
2313+ if (isArrow ) {
2314+ if (results .getArrowRecordBatch () != null ) {
2315+ try {
2316+ firstPageRows =
2317+ ArrowDeserializer .deserializeRecordBatch (
2318+ results .getArrowRecordBatch ().decodeSerializedRecordBatch (),
2319+ schema ,
2320+ arrowSchemaPojo );
2321+ } catch (IOException e ) {
2322+ throw new BigQueryException (0 , "Failed to deserialize Arrow record batch" , e );
2323+ }
2324+ } else {
2325+ firstPageRows = ImmutableList .of ();
2326+ }
2327+ } else {
2328+ firstPageRows =
2329+ transformTableData (
2330+ results .getRows (), schema , getOptions ().getDataFormatOptions ().useInt64Timestamp ());
2331+ }
2332+
21202333 return TableResult .newBuilder ()
21212334 .setSchema (schema )
21222335 .setTotalRows (numRows )
21232336 .setPageNoSchema (
21242337 new PageImpl <>(
2125- new TableDataPageFetcher (null , schema , getOptions (), null , optionMap (options )),
2338+ isArrow
2339+ ? null
2340+ : new TableDataPageFetcher (
2341+ null , schema , getOptions (), null , optionMap (options )),
21262342 null ,
2127- transformTableData (
2128- results .getRows (),
2129- schema ,
2130- getOptions ().getDataFormatOptions ().useInt64Timestamp ())))
2343+ firstPageRows ))
21312344 // Return the JobID of the successful job
21322345 .setJobId (
21332346 results .getJobReference () != null ? JobId .fromPb (results .getJobReference ()) : null )
21342347 .setQueryId (results .getQueryId ())
21352348 .setJobCreationReason (JobCreationReason .fromPb (results .getJobCreationReason ()))
2136- .setRowsInPage (results . getRows () != null ? (long ) results . getRows ( ).size () : 0L )
2349+ .setRowsInPage (firstPageRows instanceof List ? (long ) (( List <?>) firstPageRows ).size () : 0L )
21372350 .build ();
21382351 }
21392352
@@ -2207,6 +2420,10 @@ && getOptions().getOpenTelemetryTracer() != null) {
22072420
22082421 return queryRpc (projectId , content , options );
22092422 }
2423+ if (configuration .getQueryResultsFormat () == QueryResultsFormat .ARROW ) {
2424+ throw new IllegalArgumentException (
2425+ "Arrow results format is only supported for fast query path execution (e.g. no destination table, no custom clustering, etc.)." );
2426+ }
22102427 return create (JobInfo .of (jobId , configuration ), options );
22112428 } finally {
22122429 if (querySpan != null ) {
0 commit comments