Skip to content

Commit 2f4435e

Browse files
committed
feat(bigquery): integrate Arrow query response processing and stream pagination
1 parent a235d65 commit 2f4435e

2 files changed

Lines changed: 245 additions & 18 deletions

File tree

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

Lines changed: 235 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
import com.google.api.core.BetaApi;
2424
import com.google.api.core.InternalApi;
25+
import com.google.api.gax.core.FixedCredentialsProvider;
2526
import com.google.api.gax.paging.Page;
27+
import com.google.api.gax.rpc.ServerStream;
2628
import com.google.api.services.bigquery.model.ErrorProto;
2729
import com.google.api.services.bigquery.model.GetQueryResultsResponse;
2830
import com.google.api.services.bigquery.model.ProjectList;
@@ -43,6 +45,10 @@
4345
import com.google.cloud.bigquery.InsertAllRequest.RowToInsert;
4446
import com.google.cloud.bigquery.spi.v2.BigQueryRpc;
4547
import 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;
4652
import com.google.common.annotations.VisibleForTesting;
4753
import com.google.common.base.Function;
4854
import com.google.common.base.Strings;
@@ -57,6 +63,7 @@
5763
import io.opentelemetry.context.Scope;
5864
import java.io.IOException;
5965
import java.util.ArrayList;
66+
import java.util.Iterator;
6067
import java.util.List;
6168
import java.util.Map;
6269
import 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) {

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryRequestInfo.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ final class QueryRequestInfo {
4646
private final DataFormatOptions formatOptions;
4747
private final String reservation;
4848
private final Long jobTimeoutMs;
49+
private final QueryResultsFormat queryResultsFormat;
50+
private final ArrowSerializationOptions arrowSerializationOptions;
4951

5052
QueryRequestInfo(
5153
QueryJobConfiguration config, com.google.cloud.bigquery.DataFormatOptions dataFormatOptions) {
@@ -66,6 +68,8 @@ final class QueryRequestInfo {
6668
this.formatOptions = dataFormatOptions.toPb();
6769
this.reservation = config.getReservation();
6870
this.jobTimeoutMs = config.getJobTimeoutMs();
71+
this.queryResultsFormat = config.getQueryResultsFormat();
72+
this.arrowSerializationOptions = config.getArrowSerializationOptions();
6973
}
7074

7175
/**
@@ -142,6 +146,12 @@ QueryRequest toPb() {
142146
if (jobTimeoutMs != null) {
143147
request.setJobTimeoutMs(jobTimeoutMs);
144148
}
149+
if (queryResultsFormat != null) {
150+
request.setQueryResultsFormat(queryResultsFormat.toString());
151+
}
152+
if (arrowSerializationOptions != null) {
153+
request.setArrowSerializationOptions(arrowSerializationOptions.toPb());
154+
}
145155
return request;
146156
}
147157

0 commit comments

Comments
 (0)