Skip to content

Commit f5601f4

Browse files
authored
fix(bigquery): preserve page token and paginate correctly in Arrow query when maxResults is set (#14469)
When `maxResults` is set on `QueryJobConfiguration` for row-based Arrow queries, the initial REST response uses it as the page size for page 1. Previously, the client treated `initialRowOffset >= maxResults` as an indication to terminate pagination early and passed `maxResults` as the total row ceiling to `ArrowQueryPageFetcher`, preventing subsequent pages from being fetched. This updates `queryRpcArrow` to preserve the page token, pass `maxResults` as the page size option to `ArrowQueryPageFetcher`, and verifies pagination behavior in both unit tests and `testQueryRowBasedWithArrowFormatMultiPage`.
1 parent bd363f6 commit f5601f4

3 files changed

Lines changed: 98 additions & 21 deletions

File tree

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import java.util.ArrayDeque;
7575
import java.util.ArrayList;
7676
import java.util.Collections;
77+
import java.util.HashMap;
7778
import java.util.Iterator;
7879
import java.util.List;
7980
import java.util.Map;
@@ -2655,11 +2656,6 @@ && getOptions().getOpenTelemetryTracer() != null) {
26552656
// Calculate row offset and determine if subsequent pages exist.
26562657
boolean hasMorePages = results.getPageToken() != null;
26572658
long initialRowOffset = (long) firstPageRows.size();
2658-
if (hasMorePages) {
2659-
if (content.getMaxResults() != null && initialRowOffset >= content.getMaxResults()) {
2660-
hasMorePages = false;
2661-
}
2662-
}
26632659

26642660
// Multi-page results: configure ArrowQueryPageFetcher for subsequent tabledata.list calls.
26652661
if (hasMorePages) {
@@ -2669,6 +2665,11 @@ && getOptions().getOpenTelemetryTracer() != null) {
26692665
}
26702666
JobId jobId = JobId.fromPb(results.getJobReference());
26712667
String cursor = results.getPageToken();
2668+
Map<BigQueryRpc.Option, Object> fetcherOptions = new HashMap<>(optionMap(options));
2669+
if (content.getMaxResults() != null
2670+
&& !fetcherOptions.containsKey(BigQueryRpc.Option.MAX_RESULTS)) {
2671+
fetcherOptions.put(BigQueryRpc.Option.MAX_RESULTS, content.getMaxResults());
2672+
}
26722673
NextPageFetcher<FieldValueList> pageFetcher =
26732674
new ArrowQueryPageFetcher(
26742675
jobId,
@@ -2677,8 +2678,8 @@ && getOptions().getOpenTelemetryTracer() != null) {
26772678
arrowSchemaPojo,
26782679
getOptions(),
26792680
initialRowOffset,
2680-
content.getMaxResults(),
2681-
optionMap(options));
2681+
null,
2682+
fetcherOptions);
26822683

26832684
return newTableResultBuilder(results)
26842685
.setSchema(schema)
@@ -3190,6 +3191,10 @@ private TableResult readArrowTableResultFromJob(
31903191
}
31913192

31923193
// Initialize the page fetcher targeting the ReadSession stream to load the first page of rows.
3194+
Map<BigQueryRpc.Option, Object> fetcherOptions = new HashMap<>(optionMap(options));
3195+
if (maxResults != null && !fetcherOptions.containsKey(BigQueryRpc.Option.MAX_RESULTS)) {
3196+
fetcherOptions.put(BigQueryRpc.Option.MAX_RESULTS, maxResults);
3197+
}
31933198
ArrowQueryPageFetcher pageFetcher =
31943199
new ArrowQueryPageFetcher(
31953200
completedJob.getJobId(),
@@ -3199,8 +3204,8 @@ private TableResult readArrowTableResultFromJob(
31993204
arrowSchemaPojo,
32003205
getOptions(),
32013206
0L,
3202-
maxResults,
3203-
optionMap(options));
3207+
null,
3208+
fetcherOptions);
32043209

32053210
Page<FieldValueList> firstPage = pageFetcher.getNextPage();
32063211
List<FieldValueList> firstPageRows =

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/BigQueryImplTest.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3354,7 +3354,8 @@ void testQueryWithArrowFormatMultiplePagesWithMaxResults()
33543354
.build();
33553355
ReadRowsResponse streamResponse =
33563356
ReadRowsResponse.newBuilder().setArrowRecordBatch(protoBatch).build();
3357-
when(mockServerStream.iterator()).thenReturn(ImmutableList.of(streamResponse).iterator());
3357+
when(mockServerStream.iterator())
3358+
.thenAnswer(invocation -> ImmutableList.of(streamResponse).iterator());
33583359

33593360
BigQueryReadClient mockReadClient =
33603361
mock(BigQueryReadClient.class, withSettings().withoutAnnotations());
@@ -3375,25 +3376,28 @@ void testQueryWithArrowFormatMultiplePagesWithMaxResults()
33753376
Page<FieldValueList> page2 = result.getNextPage();
33763377
assertNotNull(page2);
33773378
List<FieldValueList> page2Rows = ImmutableList.copyOf(page2.getValues());
3378-
// Since maxResults is 2 and initialRowOffset is 1, page2 should only contain 1 row even though
3379-
// stream returned 2 rows
3380-
assertEquals(1, page2Rows.size());
3379+
// Since maxResults configures the page size (2 rows), page2 contains the 2 rows from the stream
3380+
assertEquals(2, page2Rows.size());
33813381
assertEquals("2", page2Rows.get(0).get(0).getStringValue());
3382-
// Since totalRowsReturned == maxResults, hasNextPage must be false
3382+
assertEquals("3", page2Rows.get(1).get(0).getStringValue());
3383+
// End of stream reached (total 3 rows read across pages 1 and 2), hasNextPage must be false
33833384
assertFalse(page2.hasNextPage());
33843385
assertNull(page2.getNextPage());
33853386

3386-
// When maxResults is 1, initialRowOffset (1) already reaches maxResults, so hasNextPage is
3387-
// false immediately
3387+
// When maxResults is 1, page token is still preserved for subsequent pages
33883388
QueryJobConfiguration configMax1 =
33893389
QueryJobConfiguration.newBuilder("SELECT id FROM test")
33903390
.setQueryResultsFormat(QueryResultsFormat.ARROW)
33913391
.setMaxResults(1L)
33923392
.build();
33933393
TableResult resultMax1 = bigquery.query(configMax1);
33943394
assertNotNull(resultMax1);
3395-
assertFalse(resultMax1.hasNextPage());
3396-
assertNull(resultMax1.getNextPage());
3395+
assertTrue(resultMax1.hasNextPage());
3396+
Page<FieldValueList> page2Max1 = resultMax1.getNextPage();
3397+
assertNotNull(page2Max1);
3398+
List<FieldValueList> page2Max1Rows = ImmutableList.copyOf(page2Max1.getValues());
3399+
assertEquals(1, page2Max1Rows.size());
3400+
assertEquals("2", page2Max1Rows.get(0).get(0).getStringValue());
33973401
}
33983402

33993403
@Test

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7570,20 +7570,32 @@ void testQueryRowBasedWithArrowFormat() throws InterruptedException {
75707570

75717571
@Test
75727572
void testQueryRowBasedWithArrowFormatMultiPage() throws InterruptedException {
7573+
// Under fast-query execution, the initial REST response defaults to a 10 MB payload limit.
7574+
// Setting maxResults limits the initial page to 5,000 rows, forcing the remaining 10,000 rows
7575+
// to stream across multiple pages via tabledata.list with Arrow format.
75737576
String query = "SELECT x FROM UNNEST(GENERATE_ARRAY(1, 15000)) AS x";
75747577
QueryJobConfiguration config =
75757578
QueryJobConfiguration.newBuilder(query)
75767579
.setQueryResultsFormat(QueryResultsFormat.ARROW)
75777580
.setJobCreationMode(JobCreationMode.JOB_CREATION_OPTIONAL)
7581+
.setMaxResults(5000L)
75787582
.build();
75797583
TableResult result = bigquery.query(config);
75807584
assertNotNull(result);
75817585
assertEquals(15000, result.getTotalRows());
7586+
7587+
int pageCount = 0;
75827588
long count = 0;
7583-
for (FieldValueList row : result.iterateAll()) {
7584-
count++;
7585-
assertEquals(count, row.get("x").getLongValue());
7589+
TableResult currentPage = result;
7590+
while (currentPage != null) {
7591+
pageCount++;
7592+
for (FieldValueList row : currentPage.getValues()) {
7593+
count++;
7594+
assertEquals(count, row.get("x").getLongValue());
7595+
}
7596+
currentPage = currentPage.hasNextPage() ? currentPage.getNextPage() : null;
75867597
}
7598+
assertTrue(pageCount > 1);
75877599
assertEquals(15000, count);
75887600
}
75897601

@@ -7635,6 +7647,62 @@ void testQueryRowBasedWithArrowFormatFallback() throws InterruptedException {
76357647
assertEquals(1786363200000000L, row.get("ts").getTimestampValue());
76367648
}
76377649

7650+
@Test
7651+
void testQueryResultsFormatArrowFallbackMultiPage() throws InterruptedException {
7652+
String query = "SELECT x FROM UNNEST(GENERATE_ARRAY(1, 15000)) AS x";
7653+
QueryJobConfiguration config =
7654+
QueryJobConfiguration.newBuilder(query)
7655+
.setQueryResultsFormat(QueryResultsFormat.ARROW)
7656+
.setMaxResults(5000L)
7657+
.build();
7658+
JobId customJobId =
7659+
JobId.of("arrow_it_fallback_mp_" + UUID.randomUUID().toString().replace("-", "_"));
7660+
try (ArrowQueryResult result = bigquery.queryArrow(config, customJobId)) {
7661+
assertNotNull(result);
7662+
assertNotNull(result.getJobId());
7663+
assertEquals(customJobId.getJob(), result.getJobId().getJob());
7664+
int batchCount = 0;
7665+
long totalRows = 0;
7666+
for (VectorSchemaRoot root : result) {
7667+
batchCount++;
7668+
totalRows += root.getRowCount();
7669+
}
7670+
assertTrue(batchCount > 1);
7671+
assertEquals(15000, totalRows);
7672+
}
7673+
}
7674+
7675+
@Test
7676+
void testQueryRowBasedWithArrowFormatFallbackMultiPage() throws InterruptedException {
7677+
String query = "SELECT x FROM UNNEST(GENERATE_ARRAY(1, 15000)) AS x";
7678+
QueryJobConfiguration config =
7679+
QueryJobConfiguration.newBuilder(query)
7680+
.setQueryResultsFormat(QueryResultsFormat.ARROW)
7681+
.setMaxResults(5000L)
7682+
.build();
7683+
JobId customJobId =
7684+
JobId.of("row_it_fallback_mp_" + UUID.randomUUID().toString().replace("-", "_"));
7685+
TableResult result = bigquery.query(config, customJobId);
7686+
assertNotNull(result);
7687+
assertNotNull(result.getJobId());
7688+
assertEquals(customJobId.getJob(), result.getJobId().getJob());
7689+
assertEquals(15000, result.getTotalRows());
7690+
7691+
int pageCount = 0;
7692+
long count = 0;
7693+
TableResult currentPage = result;
7694+
while (currentPage != null) {
7695+
pageCount++;
7696+
for (FieldValueList row : currentPage.getValues()) {
7697+
count++;
7698+
assertEquals(count, row.get("x").getLongValue());
7699+
}
7700+
currentPage = currentPage.hasNextPage() ? currentPage.getNextPage() : null;
7701+
}
7702+
assertTrue(pageCount > 1);
7703+
assertEquals(15000, count);
7704+
}
7705+
76387706
@Test
76397707
void testUniverseDomainWithInvalidUniverseDomain() {
76407708
RemoteBigQueryHelper bigqueryHelper = RemoteBigQueryHelper.create();

0 commit comments

Comments
 (0)