Skip to content

Commit 7109ecd

Browse files
perf(bigquery-jdbc): eliminate dry run to resolve statement type (#14156)
b/549675845 This PR eliminates the pre-execution dry-run query previously required to determine `StatementType` during query execution in the BigQuery JDBC driver. ### Changes 1. **Direct `StatementType` Resolution**: Updated `BigQueryStatement.runQuery()` to extract `StatementType` directly from `TableResult` (for stateless/jobless queries) or `JobStatistics` (for job-based queries), removing the dry-run query fallback. 2. **DML Row Count Optimization**: Updated `BigQueryStatement.handleQueryResult()` to retrieve `numDmlAffectedRows` directly from `TableResult`, avoiding extra `getJob()` polling RPCs for DML statements. 3. **Unit Tests**: Added test cases in `BigQueryStatementTest` verifying that `bigquery.create()` and `bigquery.getJob()` are never invoked during stateless query and DML executions.
1 parent a51bb8d commit 7109ecd

2 files changed

Lines changed: 61 additions & 24 deletions

File tree

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,16 @@ ExecuteResult executeJob(QueryJobConfiguration jobConfiguration)
602602
return new ExecuteResult(tableResult, job);
603603
}
604604

605+
private StatementType getStatementType(ExecuteResult executeResult) {
606+
if (executeResult.tableResult.getStatementType() != null) {
607+
return executeResult.tableResult.getStatementType();
608+
}
609+
if (executeResult.job != null && executeResult.job.getStatistics() instanceof QueryStatistics) {
610+
return ((QueryStatistics) executeResult.job.getStatistics()).getStatementType();
611+
}
612+
return null;
613+
}
614+
605615
/**
606616
* Execute the SQL script and sets the reference of the underlying job, passing null querySettings
607617
* will result in the FastQueryPath
@@ -620,10 +630,7 @@ void runQuery(String query, QueryJobConfiguration jobConfiguration)
620630
try {
621631
resetStatementFields();
622632
ExecuteResult executeResult = executeJob(jobConfiguration);
623-
StatementType statementType =
624-
executeResult.job == null
625-
? getStatementType(jobConfiguration)
626-
: ((QueryStatistics) executeResult.job.getStatistics()).getStatementType();
633+
StatementType statementType = getStatementType(executeResult);
627634
SqlType queryType = getQueryType(jobConfiguration, statementType);
628635
handleQueryResult(query, executeResult.tableResult, queryType, executeResult.job);
629636
} catch (InterruptedException ex) {
@@ -709,11 +716,16 @@ void handleQueryResult(String query, TableResult results, SqlType queryType, Job
709716
break;
710717
case DML:
711718
case DML_EXTRA:
712-
QueryStatistics dmlStats = getQueryStatisticsFromJob(results, job);
713-
Long dmlRowCount =
714-
(dmlStats != null && dmlStats.getNumDmlAffectedRows() != null)
715-
? dmlStats.getNumDmlAffectedRows()
716-
: 0L;
719+
Long dmlRowCount;
720+
if (results.getNumDmlAffectedRows() != null) {
721+
dmlRowCount = results.getNumDmlAffectedRows();
722+
} else {
723+
QueryStatistics dmlStats = getQueryStatisticsFromJob(results, job);
724+
dmlRowCount =
725+
(dmlStats != null && dmlStats.getNumDmlAffectedRows() != null)
726+
? dmlStats.getNumDmlAffectedRows()
727+
: 0L;
728+
}
717729
updateAffectedRowCount(dmlRowCount);
718730
break;
719731
case TCL:
@@ -773,7 +785,7 @@ private QueryStatistics getQueryStatisticsFromJob(TableResult results, Job job)
773785
throws SQLException {
774786
try {
775787
Job activeJob = job;
776-
if (activeJob == null) {
788+
if (activeJob == null && results.getJobId() != null) {
777789
activeJob = this.bigQuery.getJob(results.getJobId());
778790
}
779791
Job completedJob = (activeJob != null) ? activeJob.waitFor() : null;

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static com.google.common.truth.Truth.assertThat;
2121
import static org.junit.jupiter.api.Assertions.assertEquals;
2222
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
import static org.junit.jupiter.api.Assertions.assertNotNull;
24+
import static org.junit.jupiter.api.Assertions.assertNull;
2325
import static org.junit.jupiter.api.Assertions.assertTrue;
2426
import static org.junit.jupiter.api.Assertions.fail;
2527
import static org.mockito.ArgumentMatchers.any;
@@ -169,6 +171,10 @@ private TableResult setupMockQueryResults(JobId jobId, StatementType type, Long
169171
TableResult tableResultMock = mock(TableResult.class);
170172
doReturn(jobId).when(tableResultMock).getJobId();
171173
doReturn(Schema.of()).when(tableResultMock).getSchema();
174+
doReturn(type).when(tableResultMock).getStatementType();
175+
if (affectedRows != null) {
176+
doReturn(affectedRows).when(tableResultMock).getNumDmlAffectedRows();
177+
}
172178
doReturn(tableResultMock)
173179
.when(bigquery)
174180
.queryWithTimeout(any(QueryJobConfiguration.class), any(), any());
@@ -447,24 +453,18 @@ public void testJoblessQuery() throws SQLException, InterruptedException {
447453
TableResult tableResultMock = mock(TableResult.class);
448454
doReturn("queryId").when(tableResultMock).getQueryId();
449455
doReturn(null).when(tableResultMock).getJobId();
456+
doReturn(StatementType.SELECT).when(tableResultMock).getStatementType();
450457
doReturn(tableResultMock)
451458
.when(bigquery)
452459
.queryWithTimeout(any(QueryJobConfiguration.class), any(), any());
453460
doReturn(mock(BigQueryJsonResultSet.class))
454461
.when(joblessStatementSpy)
455462
.processJsonResultSet(eq(tableResultMock), any());
456463

457-
Job dryRunJobMock = getJobMock(null, null, StatementType.SELECT);
458-
ArgumentCaptor<JobInfo> dryRunCaptor = ArgumentCaptor.forClass(JobInfo.class);
459-
doReturn(dryRunJobMock).when(bigquery).create(dryRunCaptor.capture());
460-
461464
joblessStatementSpy.executeQuery("SELECT 1");
462465

463466
verify(bigquery).queryWithTimeout(any(QueryJobConfiguration.class), any(), any());
464-
verify(bigquery).create(any(JobInfo.class));
465-
assertTrue(
466-
Boolean.TRUE.equals(
467-
((QueryJobConfiguration) dryRunCaptor.getValue().getConfiguration()).dryRun()));
467+
verify(bigquery, Mockito.never()).create(any(JobInfo.class));
468468

469469
// 2. Test JobCreationMode=1 (jobful)
470470
Mockito.reset(bigquery);
@@ -914,6 +914,7 @@ public void testExecute_propagatesContextAndBaggage() throws Exception {
914914
TableResult tableResultMock = mock(TableResult.class);
915915
doReturn(jobId).when(tableResultMock).getJobId();
916916
doReturn(Schema.of()).when(tableResultMock).getSchema();
917+
doReturn(StatementType.SELECT).when(tableResultMock).getStatementType();
917918
return tableResultMock;
918919
})
919920
.when(bigquery)
@@ -923,8 +924,6 @@ public void testExecute_propagatesContextAndBaggage() throws Exception {
923924

924925
// Setup connection mocks to allow the statement to execute successfully
925926
doReturn(true).when(bigQueryConnection).getUseStatelessQueryMode();
926-
Job dryRunJobMock = getJobMock(null, null, StatementType.SELECT);
927-
doReturn(dryRunJobMock).when(bigquery).create(Mockito.any(JobInfo.class));
928927

929928
BigQueryJsonResultSet resultSetMock = mock(BigQueryJsonResultSet.class);
930929
doReturn(resultSetMock)
@@ -937,6 +936,7 @@ public void testExecute_propagatesContextAndBaggage() throws Exception {
937936
// Verify the SDK call actually occurred
938937
verify(bigquery)
939938
.queryWithTimeout(Mockito.any(QueryJobConfiguration.class), Mockito.any(), Mockito.any());
939+
verify(bigquery, Mockito.never()).create(Mockito.any(JobInfo.class));
940940
}
941941

942942
@Test
@@ -1065,13 +1065,10 @@ public void testTemporaryDatasetCreationRespectsConnectionLocation()
10651065
// 2. Mock bigQuery.getDataset to return null (triggering creation)
10661066
doReturn(null).when(bigquery).getDataset(eq(DatasetId.of("temp_dataset")));
10671067

1068-
// 2b. Mock bigQuery.create for dry run during getStatementType
1069-
Job dryRunJobMock = getJobMock(null, null, StatementType.SELECT);
1070-
doReturn(dryRunJobMock).when(bigquery).create(any(JobInfo.class));
1071-
10721068
// 3. Mock bigquery.queryWithTimeout(...) to return tableResult (so execution doesn't fail on
10731069
// query execution)
10741070
TableResult result = mock(TableResult.class);
1071+
doReturn(StatementType.SELECT).when(result).getStatementType();
10751072
doReturn(result)
10761073
.when(bigquery)
10771074
.queryWithTimeout(any(QueryJobConfiguration.class), any(JobId.class), any());
@@ -1091,4 +1088,32 @@ public void testTemporaryDatasetCreationRespectsConnectionLocation()
10911088
assertEquals("temp_dataset", createdDatasetInfo.getDatasetId().getDataset());
10921089
assertEquals("europe-west3", createdDatasetInfo.getLocation());
10931090
}
1091+
1092+
@Test
1093+
public void testStatelessQueryExecutionDoesNotInvokeDryRun() throws Exception {
1094+
TableResult tableResultMock = setupMockQueryResults(null, StatementType.SELECT, null);
1095+
BigQueryStatement statementSpy = Mockito.spy(bigQueryStatement);
1096+
doReturn(mock(BigQueryJsonResultSet.class))
1097+
.when(statementSpy)
1098+
.processJsonResultSet(eq(tableResultMock), any());
1099+
1100+
boolean hasResultSet = statementSpy.execute("SELECT 1");
1101+
1102+
assertTrue(hasResultSet);
1103+
assertNotNull(statementSpy.getResultSet());
1104+
verify(bigquery, Mockito.never()).create(any(JobInfo.class));
1105+
}
1106+
1107+
@Test
1108+
public void testStatelessDmlExecutionUsesTableResultWithoutDryRunOrGetJob() throws Exception {
1109+
setupMockQueryResults(null, StatementType.UPDATE, 15L);
1110+
1111+
int updatedCount = bigQueryStatement.executeUpdate("UPDATE dataset.table SET col = 1");
1112+
1113+
assertEquals(15, updatedCount);
1114+
assertEquals(15L, bigQueryStatement.getLargeUpdateCount());
1115+
assertNull(bigQueryStatement.getResultSet());
1116+
verify(bigquery, Mockito.never()).create(any(JobInfo.class));
1117+
verify(bigquery, Mockito.never()).getJob(any(JobId.class));
1118+
}
10941119
}

0 commit comments

Comments
 (0)