Skip to content

Commit 1bf3998

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

4 files changed

Lines changed: 300 additions & 21 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/BigQueryImpl.java

Lines changed: 257 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,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

Comments
 (0)