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 ;
@@ -264,6 +270,186 @@ public Page<FieldValueList> getNextPage() {
264270 }
265271 }
266272
273+ private static class ArrowQueryPageFetcher implements NextPageFetcher <FieldValueList > {
274+ private static final long serialVersionUID = 1L ;
275+
276+ private static final long DEFAULT_PAGE_SIZE = 10000L ;
277+
278+ private final JobId jobId ;
279+ private final Schema schema ;
280+ private final String arrowSchemaJson ;
281+ private final BigQueryOptions serviceOptions ;
282+ private final long maxResults ;
283+
284+ private transient Object arrowSchemaPojo ;
285+ private final long totalRowsReturned ;
286+
287+ ArrowQueryPageFetcher (
288+ JobId jobId ,
289+ Schema schema ,
290+ Object arrowSchemaPojo ,
291+ BigQueryOptions serviceOptions ,
292+ long initialRowOffset ,
293+ Long maxResults ) {
294+ this .jobId = jobId ;
295+ this .schema = schema ;
296+ this .arrowSchemaJson = ArrowDeserializer .arrowSchemaToJson (arrowSchemaPojo );
297+ this .arrowSchemaPojo = arrowSchemaPojo ;
298+ this .serviceOptions = serviceOptions ;
299+ this .totalRowsReturned = initialRowOffset ;
300+ this .maxResults = maxResults != null ? maxResults : Long .MAX_VALUE ;
301+ }
302+
303+ @ Override
304+ public Page <FieldValueList > getNextPage () {
305+ if (totalRowsReturned >= maxResults ) {
306+ return null ;
307+ }
308+
309+ List <FieldValueList > rowBatch =
310+ new ArrayList <>((int ) Math .min (DEFAULT_PAGE_SIZE , maxResults - totalRowsReturned ));
311+ boolean hasMore = false ;
312+ BigQueryReadClient ownedClient = null ;
313+
314+ try {
315+ if (arrowSchemaPojo == null && arrowSchemaJson != null ) {
316+ arrowSchemaPojo = ArrowDeserializer .jsonToArrowSchema (arrowSchemaJson );
317+ }
318+
319+ String location =
320+ jobId .getLocation () != null ? jobId .getLocation () : serviceOptions .getLocation ();
321+ if (location == null ) {
322+ throw new BigQueryException (
323+ 0 , "Job location is required to read Arrow rows from storage stream" );
324+ }
325+
326+ BigQuery service = serviceOptions .getService ();
327+ BigQueryReadClient client ;
328+ if (service instanceof BigQueryImpl ) {
329+ client = ((BigQueryImpl ) service ).getBigQueryReadClient ();
330+ } else {
331+ BigQueryReadSettings .Builder settingsBuilder = BigQueryReadSettings .newBuilder ();
332+ configureReadSettings (settingsBuilder , serviceOptions );
333+ ownedClient = BigQueryReadClient .create (settingsBuilder .build ());
334+ client = ownedClient ;
335+ }
336+
337+ String projectId =
338+ jobId .getProject () != null ? jobId .getProject () : serviceOptions .getProjectId ();
339+ if (projectId == null ) {
340+ throw new BigQueryException (
341+ 0 , "Project ID is required to read Arrow rows from storage stream" );
342+ }
343+ String streamName =
344+ String .format (
345+ "projects/%s/locations/%s/jobs/%s/streams/_default" ,
346+ projectId , location , jobId .getJob ());
347+
348+ ReadRowsRequest readRowsRequest =
349+ ReadRowsRequest .newBuilder ()
350+ .setReadStream (streamName )
351+ .setOffset (totalRowsReturned )
352+ .build ();
353+
354+ ServerStream <ReadRowsResponse > stream = client .readRowsCallable ().call (readRowsRequest );
355+ try {
356+ hasMore =
357+ ArrowDeserializer .loadArrowRows (
358+ stream .iterator (),
359+ arrowSchemaPojo ,
360+ arrowSchemaJson ,
361+ schema ,
362+ rowBatch ,
363+ DEFAULT_PAGE_SIZE ,
364+ totalRowsReturned ,
365+ maxResults );
366+ } finally {
367+ stream .cancel ();
368+ }
369+ } catch (BigQueryException e ) {
370+ throw e ;
371+ } catch (Exception e ) {
372+ throw new BigQueryException (0 , "Failed to read Arrow rows from storage stream" , e );
373+ } finally {
374+ if (ownedClient != null ) {
375+ try {
376+ ownedClient .close ();
377+ } catch (Exception e ) {
378+ // ignore
379+ }
380+ }
381+ }
382+
383+ if (rowBatch .isEmpty ()) {
384+ return null ;
385+ }
386+
387+ long nextOffset = totalRowsReturned + rowBatch .size ();
388+ String nextPageToken = hasMore ? String .valueOf (nextOffset ) : null ;
389+ ArrowQueryPageFetcher nextPageFetcher =
390+ new ArrowQueryPageFetcher (
391+ jobId , schema , arrowSchemaPojo , serviceOptions , nextOffset , maxResults );
392+ return new PageImpl <>(nextPageFetcher , nextPageToken , rowBatch );
393+ }
394+ }
395+
396+ private final java .util .concurrent .locks .ReentrantLock readClientLock =
397+ new java .util .concurrent .locks .ReentrantLock ();
398+ private transient BigQueryReadClient bqReadClient ;
399+
400+ BigQueryReadClient getBigQueryReadClient () throws IOException {
401+ readClientLock .lock ();
402+ try {
403+ if (bqReadClient == null ) {
404+ BigQueryReadSettings .Builder settingsBuilder = BigQueryReadSettings .newBuilder ();
405+ configureReadSettings (settingsBuilder , getOptions ());
406+ bqReadClient = BigQueryReadClient .create (settingsBuilder .build ());
407+ }
408+ return bqReadClient ;
409+ } finally {
410+ readClientLock .unlock ();
411+ }
412+ }
413+
414+ private static void configureReadSettings (
415+ BigQueryReadSettings .Builder settingsBuilder , BigQueryOptions options ) {
416+ if (options .getCredentials () != null ) {
417+ settingsBuilder .setCredentialsProvider (
418+ FixedCredentialsProvider .create (options .getCredentials ()));
419+ }
420+ if (options .getUniverseDomain () != null ) {
421+ settingsBuilder .setUniverseDomain (options .getUniverseDomain ());
422+ }
423+ if (options .getHost () != null ) {
424+ String host = options .getHost ();
425+ String target = host ;
426+ if (target .contains ("://" )) {
427+ target = java .net .URI .create (target ).getAuthority ();
428+ }
429+ com .google .common .net .HostAndPort hostAndPort =
430+ com .google .common .net .HostAndPort .fromString (target );
431+ String endpointHost = hostAndPort .getHost ();
432+ if (endpointHost .contains ("bigquery.googleapis.com" )) {
433+ endpointHost =
434+ endpointHost .replace ("bigquery.googleapis.com" , "bigquerystorage.googleapis.com" );
435+ } else if (endpointHost .contains ("bigquery.private.googleapis.com" )) {
436+ endpointHost =
437+ endpointHost .replace (
438+ "bigquery.private.googleapis.com" , "bigquerystorage.private.googleapis.com" );
439+ } else if (endpointHost .startsWith ("bigquery." )) {
440+ endpointHost = endpointHost .replaceFirst ("^bigquery\\ ." , "bigquerystorage." );
441+ }
442+ int port = hostAndPort .getPortOrDefault (443 );
443+ settingsBuilder .setEndpoint (endpointHost + ":" + port );
444+ if (endpointHost .contains ("localhost" ) || endpointHost .contains ("127.0.0.1" )) {
445+ settingsBuilder .setTransportChannelProvider (
446+ BigQueryReadSettings .defaultGrpcTransportProviderBuilder ()
447+ .setChannelConfigurator (io .grpc .ManagedChannelBuilder ::usePlaintext )
448+ .build ());
449+ }
450+ }
451+ }
452+
267453 private final HttpBigQueryRpc bigQueryRpc ;
268454
269455 private static final BigQueryRetryConfig EMPTY_RETRY_CONFIG =
@@ -2077,8 +2263,26 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20772263
20782264 long numRows ;
20792265 Schema schema ;
2080- if (results .getJobComplete () && results .getSchema () != null ) {
2081- schema = Schema .fromPb (results .getSchema ());
2266+ boolean isArrow = false ;
2267+ Object arrowSchemaPojo = null ;
2268+
2269+ if (results .getJobComplete ()) {
2270+ if (results .getArrowSchema () != null ) {
2271+ isArrow = true ;
2272+ try {
2273+ arrowSchemaPojo =
2274+ ArrowDeserializer .deserializeSchema (
2275+ results .getArrowSchema ().decodeSerializedSchema ());
2276+ schema = ArrowDeserializer .arrowSchemaToBigQuerySchema (arrowSchemaPojo );
2277+ } catch (IOException e ) {
2278+ throw new BigQueryException (0 , "Failed to deserialize Arrow schema from response" , e );
2279+ }
2280+ } else if (results .getSchema () != null ) {
2281+ schema = Schema .fromPb (results .getSchema ());
2282+ } else {
2283+ schema = null ;
2284+ }
2285+
20822286 if (results .getNumDmlAffectedRows () == null && results .getTotalRows () == null ) {
20832287 numRows = 0L ;
20842288 } else if (results .getNumDmlAffectedRows () != null ) {
@@ -2095,45 +2299,75 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20952299 return job ;
20962300 }
20972301
2302+ List <FieldValueList > firstPageRows ;
2303+ if (isArrow ) {
2304+ if (results .getArrowRecordBatch () != null ) {
2305+ try {
2306+ firstPageRows =
2307+ ArrowDeserializer .deserializeRecordBatch (
2308+ results .getArrowRecordBatch ().decodeSerializedRecordBatch (),
2309+ schema ,
2310+ (org .apache .arrow .vector .types .pojo .Schema ) arrowSchemaPojo );
2311+ } catch (IOException e ) {
2312+ throw new BigQueryException (0 , "Failed to deserialize Arrow record batch" , e );
2313+ }
2314+ } else {
2315+ firstPageRows = ImmutableList .of ();
2316+ }
2317+ } else {
2318+ firstPageRows =
2319+ ImmutableList .copyOf (
2320+ transformTableData (
2321+ results .getRows (),
2322+ schema ,
2323+ getOptions ().getDataFormatOptions ().useInt64Timestamp ()));
2324+ }
2325+
20982326 if (results .getPageToken () != null ) {
20992327 JobId jobId = JobId .fromPb (results .getJobReference ());
21002328 String cursor = results .getPageToken ();
2329+
2330+ NextPageFetcher <FieldValueList > pageFetcher ;
2331+ if (isArrow ) {
2332+ long initialRowOffset = (long ) firstPageRows .size ();
2333+ Map <BigQueryRpc .Option , ?> optionsMap = optionMap (options );
2334+ Number maxResultsOpt = (Number ) optionsMap .get (BigQueryRpc .Option .MAX_RESULTS );
2335+ Long maxResults = maxResultsOpt != null ? maxResultsOpt .longValue () : null ;
2336+ pageFetcher =
2337+ new ArrowQueryPageFetcher (
2338+ jobId , schema , arrowSchemaPojo , getOptions (), initialRowOffset , maxResults );
2339+ } else {
2340+ pageFetcher = new QueryPageFetcher (jobId , schema , getOptions (), cursor , optionMap (options ));
2341+ }
2342+
21012343 return TableResult .newBuilder ()
21022344 .setSchema (schema )
21032345 .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 ())))
2346+ .setPageNoSchema (new PageImpl <>(pageFetcher , cursor , firstPageRows ))
21132347 .setJobId (jobId )
21142348 .setQueryId (results .getQueryId ())
21152349 .setJobCreationReason (JobCreationReason .fromPb (results .getJobCreationReason ()))
2116- .setRowsInPage (results . getRows () != null ? ( long ) results . getRows (). size () : 0L )
2350+ .setRowsInPage (( long ) firstPageRows . size ())
21172351 .build ();
21182352 }
2119- // only 1 page of result
2353+
21202354 return TableResult .newBuilder ()
21212355 .setSchema (schema )
21222356 .setTotalRows (numRows )
21232357 .setPageNoSchema (
21242358 new PageImpl <>(
2125- new TableDataPageFetcher (null , schema , getOptions (), null , optionMap (options )),
2359+ isArrow
2360+ ? null
2361+ : new TableDataPageFetcher (
2362+ null , schema , getOptions (), null , optionMap (options )),
21262363 null ,
2127- transformTableData (
2128- results .getRows (),
2129- schema ,
2130- getOptions ().getDataFormatOptions ().useInt64Timestamp ())))
2364+ firstPageRows ))
21312365 // Return the JobID of the successful job
21322366 .setJobId (
21332367 results .getJobReference () != null ? JobId .fromPb (results .getJobReference ()) : null )
21342368 .setQueryId (results .getQueryId ())
21352369 .setJobCreationReason (JobCreationReason .fromPb (results .getJobCreationReason ()))
2136- .setRowsInPage (results . getRows () != null ? ( long ) results . getRows (). size () : 0L )
2370+ .setRowsInPage (( long ) firstPageRows . size ())
21372371 .build ();
21382372 }
21392373
@@ -2207,6 +2441,10 @@ && getOptions().getOpenTelemetryTracer() != null) {
22072441
22082442 return queryRpc (projectId , content , options );
22092443 }
2444+ if (configuration .getQueryResultsFormat () == QueryResultsFormat .ARROW ) {
2445+ throw new IllegalArgumentException (
2446+ "Arrow results format is only supported for fast query path execution (e.g. no destination table, no custom clustering, etc.)." );
2447+ }
22102448 return create (JobInfo .of (jobId , configuration ), options );
22112449 } finally {
22122450 if (querySpan != null ) {
0 commit comments