Skip to content

Commit cd29144

Browse files
committed
feat(bigquery): integrate Arrow query response processing and stream pagination
1 parent 7fc0c80 commit cd29144

5 files changed

Lines changed: 320 additions & 22 deletions

File tree

java-bigquery/google-cloud-bigquery/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@
120120
<artifactId>arrow-memory-netty</artifactId>
121121
</dependency>
122122

123+
<dependency>
124+
<groupId>com.google.api</groupId>
125+
<artifactId>gax-grpc</artifactId>
126+
</dependency>
127+
<dependency>
128+
<groupId>io.grpc</groupId>
129+
<artifactId>grpc-api</artifactId>
130+
</dependency>
131+
123132
<dependency>
124133
<groupId>com.google.errorprone</groupId>
125134
<artifactId>error_prone_annotations</artifactId>

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
*
4141
* @see <a href="https://cloud.google.com/bigquery/what-is-bigquery">Google Cloud BigQuery</a>
4242
*/
43-
public interface BigQuery extends Service<BigQueryOptions> {
43+
public interface BigQuery extends Service<BigQueryOptions>, AutoCloseable {
44+
45+
@Override
46+
default void close() {}
4447

4548
/**
4649
* Fields of a BigQuery Dataset resource.

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

Lines changed: 273 additions & 19 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;
@@ -264,6 +270,202 @@ 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+
@Override
401+
public void close() {
402+
readClientLock.lock();
403+
try {
404+
if (bqReadClient != null) {
405+
try {
406+
bqReadClient.close();
407+
} finally {
408+
bqReadClient = null;
409+
}
410+
}
411+
} finally {
412+
readClientLock.unlock();
413+
}
414+
}
415+
416+
BigQueryReadClient getBigQueryReadClient() throws IOException {
417+
readClientLock.lock();
418+
try {
419+
if (bqReadClient == null) {
420+
BigQueryReadSettings.Builder settingsBuilder = BigQueryReadSettings.newBuilder();
421+
configureReadSettings(settingsBuilder, getOptions());
422+
bqReadClient = BigQueryReadClient.create(settingsBuilder.build());
423+
}
424+
return bqReadClient;
425+
} finally {
426+
readClientLock.unlock();
427+
}
428+
}
429+
430+
private static void configureReadSettings(
431+
BigQueryReadSettings.Builder settingsBuilder, BigQueryOptions options) {
432+
if (options.getCredentials() != null) {
433+
settingsBuilder.setCredentialsProvider(
434+
FixedCredentialsProvider.create(options.getCredentials()));
435+
}
436+
if (options.getUniverseDomain() != null) {
437+
settingsBuilder.setUniverseDomain(options.getUniverseDomain());
438+
}
439+
if (options.getHost() != null) {
440+
String host = options.getHost();
441+
String target = host;
442+
if (target.contains("://")) {
443+
target = java.net.URI.create(target).getAuthority();
444+
}
445+
com.google.common.net.HostAndPort hostAndPort =
446+
com.google.common.net.HostAndPort.fromString(target);
447+
String endpointHost = hostAndPort.getHost();
448+
if (endpointHost.contains("bigquery.googleapis.com")) {
449+
endpointHost =
450+
endpointHost.replace("bigquery.googleapis.com", "bigquerystorage.googleapis.com");
451+
} else if (endpointHost.contains("bigquery.private.googleapis.com")) {
452+
endpointHost =
453+
endpointHost.replace(
454+
"bigquery.private.googleapis.com", "bigquerystorage.private.googleapis.com");
455+
} else if (endpointHost.startsWith("bigquery.")) {
456+
endpointHost = endpointHost.replaceFirst("^bigquery\\.", "bigquerystorage.");
457+
}
458+
int port = hostAndPort.getPortOrDefault(443);
459+
settingsBuilder.setEndpoint(endpointHost + ":" + port);
460+
if (endpointHost.contains("localhost") || endpointHost.contains("127.0.0.1")) {
461+
settingsBuilder.setTransportChannelProvider(
462+
BigQueryReadSettings.defaultGrpcTransportProviderBuilder()
463+
.setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext)
464+
.build());
465+
}
466+
}
467+
}
468+
267469
private final HttpBigQueryRpc bigQueryRpc;
268470

269471
private static final BigQueryRetryConfig EMPTY_RETRY_CONFIG =
@@ -2077,8 +2279,26 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20772279

20782280
long numRows;
20792281
Schema schema;
2080-
if (results.getJobComplete() && results.getSchema() != null) {
2081-
schema = Schema.fromPb(results.getSchema());
2282+
boolean isArrow = false;
2283+
Object arrowSchemaPojo = null;
2284+
2285+
if (results.getJobComplete()) {
2286+
if (results.getArrowSchema() != null) {
2287+
isArrow = true;
2288+
try {
2289+
arrowSchemaPojo =
2290+
ArrowDeserializer.deserializeSchema(
2291+
results.getArrowSchema().decodeSerializedSchema());
2292+
schema = ArrowDeserializer.arrowSchemaToBigQuerySchema(arrowSchemaPojo);
2293+
} catch (IOException e) {
2294+
throw new BigQueryException(0, "Failed to deserialize Arrow schema from response", e);
2295+
}
2296+
} else if (results.getSchema() != null) {
2297+
schema = Schema.fromPb(results.getSchema());
2298+
} else {
2299+
schema = null;
2300+
}
2301+
20822302
if (results.getNumDmlAffectedRows() == null && results.getTotalRows() == null) {
20832303
numRows = 0L;
20842304
} else if (results.getNumDmlAffectedRows() != null) {
@@ -2095,45 +2315,75 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20952315
return job;
20962316
}
20972317

2318+
List<FieldValueList> firstPageRows;
2319+
if (isArrow) {
2320+
if (results.getArrowRecordBatch() != null) {
2321+
try {
2322+
firstPageRows =
2323+
ArrowDeserializer.deserializeRecordBatch(
2324+
results.getArrowRecordBatch().decodeSerializedRecordBatch(),
2325+
schema,
2326+
(org.apache.arrow.vector.types.pojo.Schema) arrowSchemaPojo);
2327+
} catch (IOException e) {
2328+
throw new BigQueryException(0, "Failed to deserialize Arrow record batch", e);
2329+
}
2330+
} else {
2331+
firstPageRows = ImmutableList.of();
2332+
}
2333+
} else {
2334+
firstPageRows =
2335+
ImmutableList.copyOf(
2336+
transformTableData(
2337+
results.getRows(),
2338+
schema,
2339+
getOptions().getDataFormatOptions().useInt64Timestamp()));
2340+
}
2341+
20982342
if (results.getPageToken() != null) {
20992343
JobId jobId = JobId.fromPb(results.getJobReference());
21002344
String cursor = results.getPageToken();
2345+
2346+
NextPageFetcher<FieldValueList> pageFetcher;
2347+
if (isArrow) {
2348+
long initialRowOffset = (long) firstPageRows.size();
2349+
Map<BigQueryRpc.Option, ?> optionsMap = optionMap(options);
2350+
Number maxResultsOpt = (Number) optionsMap.get(BigQueryRpc.Option.MAX_RESULTS);
2351+
Long maxResults = maxResultsOpt != null ? maxResultsOpt.longValue() : null;
2352+
pageFetcher =
2353+
new ArrowQueryPageFetcher(
2354+
jobId, schema, arrowSchemaPojo, getOptions(), initialRowOffset, maxResults);
2355+
} else {
2356+
pageFetcher = new QueryPageFetcher(jobId, schema, getOptions(), cursor, optionMap(options));
2357+
}
2358+
21012359
return TableResult.newBuilder()
21022360
.setSchema(schema)
21032361
.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())))
2362+
.setPageNoSchema(new PageImpl<>(pageFetcher, cursor, firstPageRows))
21132363
.setJobId(jobId)
21142364
.setQueryId(results.getQueryId())
21152365
.setJobCreationReason(JobCreationReason.fromPb(results.getJobCreationReason()))
2116-
.setRowsInPage(results.getRows() != null ? (long) results.getRows().size() : 0L)
2366+
.setRowsInPage((long) firstPageRows.size())
21172367
.build();
21182368
}
2119-
// only 1 page of result
2369+
21202370
return TableResult.newBuilder()
21212371
.setSchema(schema)
21222372
.setTotalRows(numRows)
21232373
.setPageNoSchema(
21242374
new PageImpl<>(
2125-
new TableDataPageFetcher(null, schema, getOptions(), null, optionMap(options)),
2375+
isArrow
2376+
? null
2377+
: new TableDataPageFetcher(
2378+
null, schema, getOptions(), null, optionMap(options)),
21262379
null,
2127-
transformTableData(
2128-
results.getRows(),
2129-
schema,
2130-
getOptions().getDataFormatOptions().useInt64Timestamp())))
2380+
firstPageRows))
21312381
// Return the JobID of the successful job
21322382
.setJobId(
21332383
results.getJobReference() != null ? JobId.fromPb(results.getJobReference()) : null)
21342384
.setQueryId(results.getQueryId())
21352385
.setJobCreationReason(JobCreationReason.fromPb(results.getJobCreationReason()))
2136-
.setRowsInPage(results.getRows() != null ? (long) results.getRows().size() : 0L)
2386+
.setRowsInPage((long) firstPageRows.size())
21372387
.build();
21382388
}
21392389

@@ -2207,6 +2457,10 @@ && getOptions().getOpenTelemetryTracer() != null) {
22072457

22082458
return queryRpc(projectId, content, options);
22092459
}
2460+
if (configuration.getQueryResultsFormat() == QueryResultsFormat.ARROW) {
2461+
throw new IllegalArgumentException(
2462+
"Arrow results format is only supported for fast query path execution (e.g. no destination table, no custom clustering, etc.).");
2463+
}
22102464
return create(JobInfo.of(jobId, configuration), options);
22112465
} finally {
22122466
if (querySpan != null) {

0 commit comments

Comments
 (0)