Skip to content

Commit 95df637

Browse files
committed
feat(bigquery): add Storage Read API slow-path fallback for row-based query()
1 parent 4eff356 commit 95df637

2 files changed

Lines changed: 328 additions & 25 deletions

File tree

‎java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java‎

Lines changed: 208 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)