@@ -303,6 +303,7 @@ static class ArrowQueryPageFetcher implements NextPageFetcher<FieldValueList> {
303303 private static final long DEFAULT_PAGE_SIZE = 10000L ;
304304
305305 private final JobId jobId ;
306+ private final String customStreamName ;
306307 private final Schema schema ;
307308 private final byte [] arrowSchemaBytes ;
308309 private final BigQueryOptions serviceOptions ;
@@ -326,7 +327,30 @@ static class ArrowQueryPageFetcher implements NextPageFetcher<FieldValueList> {
326327 long initialRowOffset ,
327328 Long maxResults ,
328329 Map <BigQueryRpc .Option , ?> optionsMap ) {
330+ this (
331+ jobId ,
332+ null ,
333+ schema ,
334+ arrowSchemaBytes ,
335+ arrowSchemaPojo ,
336+ serviceOptions ,
337+ initialRowOffset ,
338+ maxResults ,
339+ optionsMap );
340+ }
341+
342+ ArrowQueryPageFetcher (
343+ JobId jobId ,
344+ String customStreamName ,
345+ Schema schema ,
346+ byte [] arrowSchemaBytes ,
347+ org .apache .arrow .vector .types .pojo .Schema arrowSchemaPojo ,
348+ BigQueryOptions serviceOptions ,
349+ long initialRowOffset ,
350+ Long maxResults ,
351+ Map <BigQueryRpc .Option , ?> optionsMap ) {
329352 this .jobId = jobId ;
353+ this .customStreamName = customStreamName ;
330354 this .schema = schema ;
331355 this .arrowSchemaBytes = arrowSchemaBytes ;
332356 this .arrowSchemaPojo = arrowSchemaPojo ;
@@ -355,13 +379,6 @@ public Page<FieldValueList> getNextPage() {
355379 List <FieldValueList > rowBatch = new ArrayList <>((int ) Math .min (pageSize , 10000L ));
356380
357381 try {
358- String location =
359- jobId .getLocation () != null ? jobId .getLocation () : serviceOptions .getLocation ();
360- if (location == null ) {
361- throw new BigQueryException (
362- 0 , "Location must be specified to read Arrow rows from storage stream" );
363- }
364-
365382 if (bqReadClient == null ) {
366383 BigQuery service = serviceOptions .getService ();
367384 if (service instanceof BigQueryImpl ) {
@@ -374,12 +391,23 @@ public Page<FieldValueList> getNextPage() {
374391 }
375392
376393 if (streamIterator == null ) {
377- String streamName =
378- String .format (
379- "projects/%s/locations/%s/jobs/%s/streams/_default" ,
380- jobId .getProject () != null ? jobId .getProject () : serviceOptions .getProjectId (),
381- location ,
382- jobId .getJob ());
394+ String streamName ;
395+ if (customStreamName != null ) {
396+ streamName = customStreamName ;
397+ } else {
398+ String location =
399+ jobId .getLocation () != null ? jobId .getLocation () : serviceOptions .getLocation ();
400+ if (location == null ) {
401+ throw new BigQueryException (
402+ 0 , "Location must be specified to read Arrow rows from storage stream" );
403+ }
404+ streamName =
405+ String .format (
406+ "projects/%s/locations/%s/jobs/%s/streams/_default" ,
407+ jobId .getProject () != null ? jobId .getProject () : serviceOptions .getProjectId (),
408+ location ,
409+ jobId .getJob ());
410+ }
383411
384412 ReadRowsRequest readRowsRequest =
385413 ReadRowsRequest .newBuilder ()
@@ -2710,8 +2738,7 @@ && getOptions().getOpenTelemetryTracer() != null) {
27102738 }
27112739
27122740 if (configuration .getQueryResultsFormat () == QueryResultsFormat .ARROW ) {
2713- throw new UnsupportedOperationException (
2714- "Arrow results format for slow query path execution is not yet supported." );
2741+ return queryFallbackArrow (jobId , configuration , options );
27152742 }
27162743
27172744 return create (JobInfo .of (jobId , configuration ), options );
@@ -2991,6 +3018,172 @@ private ArrowQueryResult createArrowQueryResultFromTable(
29913018 return ArrowQueryResultImpl .fromReadSession (readSession , jobId , client );
29923019 }
29933020
3021+ /**
3022+ * Executes a slow-path query job using {@code jobs.insert}, awaits its completion, and streams
3023+ * the result rows via the BigQuery Storage Read API in Arrow format, wrapping the decoded rows in
3024+ * a {@link TableResult}.
3025+ *
3026+ * @param jobId the job ID, or {@code null}
3027+ * @param configuration the query job configuration
3028+ * @param options query job options
3029+ * @return a {@link TableResult} containing the decoded rows and job execution metadata
3030+ * @throws InterruptedException if interrupted while awaiting job completion
3031+ * @throws BigQueryException if job execution or ReadSession creation fails
3032+ */
3033+ private TableResult queryFallbackArrow (
3034+ JobId jobId , QueryJobConfiguration configuration , JobOption ... options )
3035+ throws InterruptedException {
3036+ Job job = create (JobInfo .of (jobId , configuration ), options );
3037+ Job completedJob = job .waitFor ();
3038+
3039+ if (completedJob == null ) {
3040+ throw new BigQueryException (0 , "Job no longer exists or could not be retrieved." );
3041+ }
3042+
3043+ if (completedJob .getStatus ().getError () != null ) {
3044+ throw new BigQueryException (Collections .singletonList (completedJob .getStatus ().getError ()));
3045+ }
3046+
3047+ TableId destinationTable = null ;
3048+ if (completedJob .getConfiguration () instanceof QueryJobConfiguration ) {
3049+ destinationTable =
3050+ ((QueryJobConfiguration ) completedJob .getConfiguration ()).getDestinationTable ();
3051+ }
3052+ if (destinationTable == null ) {
3053+ destinationTable = configuration .getDestinationTable ();
3054+ }
3055+ if (destinationTable == null ) {
3056+ throw new BigQueryException (0 , "Unable to resolve destination table for fallback query" );
3057+ }
3058+
3059+ JobStatistics .QueryStatistics stats =
3060+ completedJob .getStatistics () instanceof JobStatistics .QueryStatistics
3061+ ? (JobStatistics .QueryStatistics ) completedJob .getStatistics ()
3062+ : null ;
3063+
3064+ StatementType statementType = stats != null ? stats .getStatementType () : null ;
3065+ Long totalBytesBilled = stats != null ? stats .getTotalBytesBilled () : null ;
3066+ Long totalBytesProcessed = stats != null ? stats .getTotalBytesProcessed () : null ;
3067+ Long totalSlotMs = stats != null ? stats .getTotalSlotMs () : null ;
3068+ Long numDmlAffectedRows = stats != null ? stats .getNumDmlAffectedRows () : null ;
3069+ SessionInfo sessionInfo = stats != null ? stats .getSessionInfo () : null ;
3070+
3071+ String destProject =
3072+ destinationTable .getProject () != null
3073+ ? destinationTable .getProject ()
3074+ : (completedJob .getJobId () != null && completedJob .getJobId ().getProject () != null
3075+ ? completedJob .getJobId ().getProject ()
3076+ : getOptions ().getProjectId ());
3077+ String parent = String .format ("projects/%s" , destProject );
3078+ String srcTable =
3079+ String .format (
3080+ "projects/%s/datasets/%s/tables/%s" ,
3081+ destProject , destinationTable .getDataset (), destinationTable .getTable ());
3082+
3083+ BigQueryReadClient client = getBigQueryReadClient ();
3084+ CreateReadSessionRequest request =
3085+ CreateReadSessionRequest .newBuilder ()
3086+ .setParent (parent )
3087+ .setReadSession (
3088+ ReadSession .newBuilder ().setTable (srcTable ).setDataFormat (DataFormat .ARROW ))
3089+ .setMaxStreamCount (1 )
3090+ .build ();
3091+ ReadSession readSession ;
3092+ try {
3093+ readSession = client .createReadSession (request );
3094+ } catch (Exception e ) {
3095+ throw new BigQueryException (0 , "Failed to create ReadSession for fallback query" , e );
3096+ }
3097+
3098+ org .apache .arrow .vector .types .pojo .Schema arrowSchemaPojo = null ;
3099+ byte [] arrowSchemaBytes = null ;
3100+ if (readSession .hasArrowSchema ()) {
3101+ arrowSchemaBytes = readSession .getArrowSchema ().getSerializedSchema ().toByteArray ();
3102+ try {
3103+ arrowSchemaPojo = ArrowDeserializer .deserializeSchema (arrowSchemaBytes );
3104+ } catch (IOException e ) {
3105+ throw new BigQueryException (0 , "Failed to deserialize Arrow schema from ReadSession" , e );
3106+ }
3107+ }
3108+ Schema schema =
3109+ arrowSchemaPojo != null
3110+ ? ArrowPojoUtils .arrowSchemaToBigQuerySchema (arrowSchemaPojo )
3111+ : (stats != null ? stats .getSchema () : null );
3112+
3113+ String streamName =
3114+ readSession .getStreamsCount () > 0 ? readSession .getStreams (0 ).getName () : null ;
3115+
3116+ if (streamName == null ) {
3117+ return TableResult .newBuilder ()
3118+ .setSchema (schema )
3119+ .setTotalRows (numDmlAffectedRows != null ? numDmlAffectedRows : 0L )
3120+ .setPageNoSchema (
3121+ new PageImpl <>(
3122+ new TableDataPageFetcher (null , schema , getOptions (), null , optionMap (options )),
3123+ null ,
3124+ ImmutableList .of ()))
3125+ .setJobId (completedJob .getJobId ())
3126+ .setRowsInPage (0L )
3127+ .setStatementType (statementType )
3128+ .setTotalBytesBilled (totalBytesBilled )
3129+ .setTotalBytesProcessed (totalBytesProcessed )
3130+ .setTotalSlotMs (totalSlotMs )
3131+ .setNumDmlAffectedRows (numDmlAffectedRows )
3132+ .setSessionInfo (sessionInfo )
3133+ .build ();
3134+ }
3135+
3136+ ArrowQueryPageFetcher pageFetcher =
3137+ new ArrowQueryPageFetcher (
3138+ completedJob .getJobId (),
3139+ streamName ,
3140+ schema ,
3141+ arrowSchemaBytes ,
3142+ arrowSchemaPojo ,
3143+ getOptions (),
3144+ 0L ,
3145+ configuration .getMaxResults (),
3146+ optionMap (options ));
3147+
3148+ Page <FieldValueList > firstPage = pageFetcher .getNextPage ();
3149+ List <FieldValueList > firstPageRows =
3150+ firstPage != null ? ImmutableList .copyOf (firstPage .getValues ()) : ImmutableList .of ();
3151+ long rowsInPage = (long ) firstPageRows .size ();
3152+
3153+ Table destTable = null ;
3154+ try {
3155+ destTable = getTable (destinationTable );
3156+ } catch (Exception e ) {
3157+ // Non-fatal table lookup failure
3158+ }
3159+ long totalRows =
3160+ numDmlAffectedRows != null
3161+ ? numDmlAffectedRows
3162+ : (destTable != null && destTable .getNumRows () != null
3163+ ? destTable .getNumRows ().longValue ()
3164+ : rowsInPage );
3165+
3166+ return TableResult .newBuilder ()
3167+ .setSchema (schema )
3168+ .setTotalRows (totalRows )
3169+ .setPageNoSchema (
3170+ firstPage != null
3171+ ? firstPage
3172+ : new PageImpl <>(
3173+ new TableDataPageFetcher (null , schema , getOptions (), null , optionMap (options )),
3174+ null ,
3175+ ImmutableList .of ()))
3176+ .setJobId (completedJob .getJobId ())
3177+ .setRowsInPage (rowsInPage )
3178+ .setStatementType (statementType )
3179+ .setTotalBytesBilled (totalBytesBilled )
3180+ .setTotalBytesProcessed (totalBytesProcessed )
3181+ .setTotalSlotMs (totalSlotMs )
3182+ .setNumDmlAffectedRows (numDmlAffectedRows )
3183+ .setSessionInfo (sessionInfo )
3184+ .build ();
3185+ }
3186+
29943187 @ Override
29953188 public QueryResponse getQueryResults (JobId jobId , QueryResultsOption ... options ) {
29963189 Map <BigQueryRpc .Option , ?> optionsMap = optionMap (options );
0 commit comments