From 5ddca55405bfbeb6715ed7d9ec418076740fffa6 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 4 Sep 2026 14:05:58 +0000 Subject: [PATCH] fix(bigquery-jdbc): resolve `statementType` via `getJob` fallback to avoid post-execution dry run for DDL --- .../bigquery/jdbc/BigQueryStatement.java | 11 +++++++ .../bigquery/jdbc/BigQueryStatementTest.java | 23 ++++++++++++++ .../bigquery/jdbc/it/ITStatementTest.java | 30 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index 84ac6854b6b8..3fd724c820d9 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -617,12 +617,23 @@ private void saveSessionIdIfPresent(TableResult tableResult) { } private StatementType getStatementType(ExecuteResult executeResult) { + // Fast path: Read statementType directly from TableResult if (executeResult.tableResult.getStatementType() != null) { return executeResult.tableResult.getStatementType(); } + // Jobful path: Read statementType from Job statistics when executed via JobCreationMode=1 or + // jobs.insert if (executeResult.job != null && executeResult.job.getStatistics() instanceof QueryStatistics) { return ((QueryStatistics) executeResult.job.getStatistics()).getStatementType(); } + // Fallback path: Lazily fetch completed Job metadata to resolve statementType without dry + // runs if omitted in TableResult + if (executeResult.tableResult.getJobId() != null && this.bigQuery != null) { + Job job = this.bigQuery.getJob(executeResult.tableResult.getJobId()); + if (job != null && job.getStatistics() instanceof QueryStatistics) { + return ((QueryStatistics) job.getStatistics()).getStatementType(); + } + } return null; } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java index 15e99f6699a2..00b401440a2a 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java @@ -1136,4 +1136,27 @@ public void testStatelessDmlExecutionUsesTableResultWithoutDryRunOrGetJob() thro verify(bigquery, Mockito.never()).create(any(JobInfo.class)); verify(bigquery, Mockito.never()).getJob(any(JobId.class)); } + + @Test + public void testNullStatementTypeInTableResultFallsBackToGetJobWithoutDryRun() throws Exception { + TableResult tableResultMock = mock(TableResult.class); + doReturn(this.jobId).when(tableResultMock).getJobId(); + doReturn(null).when(tableResultMock).getStatementType(); + doReturn(0L).when(tableResultMock).getTotalRows(); + doReturn(Schema.of()).when(tableResultMock).getSchema(); + + doReturn(tableResultMock) + .when(bigquery) + .queryWithTimeout(any(QueryJobConfiguration.class), any(), any()); + + Job jobMock = getJobMock(null, null, StatementType.CREATE_TABLE); + doReturn(jobMock).when(bigquery).getJob(eq(this.jobId)); + + boolean result = bigQueryStatement.execute("CREATE TABLE dataset.my_table (x INT64)"); + + assertFalse(result); + assertNull(bigQueryStatement.getResultSet()); + verify(bigquery, Mockito.times(1)).getJob(eq(this.jobId)); + verify(bigquery, Mockito.never()).create(any(JobInfo.class)); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITStatementTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITStatementTest.java index 9790284b664e..e73f2395b493 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITStatementTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITStatementTest.java @@ -460,4 +460,34 @@ public void testSessionPersistenceAcrossQueries() throws SQLException { } } } + + @Test + public void testNonIdempotentCreateTableAndDrop() throws SQLException { + String tempTableName = "NON_IDEMPOTENT_TABLE_" + Math.abs(random.nextInt(100000)); + String createTableQuery = + String.format("CREATE TABLE %s.%s (`id` INT64, `name` STRING);", DATASET, tempTableName); + String dropTableQuery = String.format("DROP TABLE %s.%s;", DATASET, tempTableName); + + try (Connection connection = DriverManager.getConnection(ITBase.connectionUrl); + Statement statement = connection.createStatement()) { + boolean hasResultSet = statement.execute(createTableQuery); + assertFalse(hasResultSet); + assertEquals(0, statement.getUpdateCount()); + + try (ResultSet rs = + statement.executeQuery( + String.format("SELECT count(*) FROM %s.%s;", DATASET, tempTableName))) { + assertTrue(rs.next()); + assertEquals(0L, rs.getLong(1)); + assertFalse(rs.next()); + } + } finally { + try (Connection connection = DriverManager.getConnection(ITBase.connectionUrl); + Statement statement = connection.createStatement()) { + statement.execute(dropTableQuery); + } catch (SQLException ignored) { + // Ignore cleanup exception if table was not created + } + } + } }