diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java index c057cdaca6..320daa03a2 100644 --- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryImpl.java @@ -2015,6 +2015,7 @@ public com.google.api.services.bigquery.model.QueryResponse call() getOptions().getDataFormatOptions().useInt64Timestamp()))) .setJobId(jobId) .setQueryId(results.getQueryId()) + .setJobCreationReason(JobCreationReason.fromPb(results.getJobCreationReason())) .build(); } // only 1 page of result @@ -2033,6 +2034,7 @@ public com.google.api.services.bigquery.model.QueryResponse call() .setJobId( results.getJobReference() != null ? JobId.fromPb(results.getJobReference()) : null) .setQueryId(results.getQueryId()) + .setJobCreationReason(JobCreationReason.fromPb(results.getJobCreationReason())) .build(); } diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobCreationReason.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobCreationReason.java new file mode 100644 index 0000000000..296c5cc049 --- /dev/null +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/JobCreationReason.java @@ -0,0 +1,80 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigquery; + +import javax.annotation.Nullable; + +/** + * Maps to JobCreationReason + * when used with {@link + * com.google.cloud.bigquery.QueryJobConfiguration.JobCreationMode#JOB_CREATION_OPTIONAL}. + * + *
The code indicates the high level reason why a job was created. The default is `UNKNOWN` if + * there is no mapping found between the server response and the client library. + */ +public class JobCreationReason { + + public enum Code { + REQUESTED("REQUESTED"), + LONG_RUNNING("LONG_RUNNING"), + LARGE_RESULTS("LARGE_RESULTS"), + OTHER("OTHER"), + UNKNOWN("UNKNOWN"); + + private final String reason; + + Code(String reason) { + this.reason = reason; + } + + /** + * Maps the server code to BQ code. Returns {@link Code#UNKNOWN} if the mapping does not exist. + */ + static Code fromValue(@Nullable String reason) { + for (JobCreationReason.Code code : Code.values()) { + if (code.reason.equals(reason)) { + return code; + } + } + return UNKNOWN; + } + } + + @Nullable private final Code code; + + JobCreationReason(Code code) { + this.code = code; + } + + static JobCreationReason fromPb( + com.google.api.services.bigquery.model.JobCreationReason jobCreationReason) { + // JobCreationReason may be null if the JobCreationMode is specified to be Optional + // Note: JobCreationMode.Optional may also end up creating a job depending on the + // query complexity and other factors. + if (jobCreationReason == null) { + return null; + } + return new JobCreationReason(Code.fromValue(jobCreationReason.getCode())); + } + + /** + * @return JobCreationReason code or {@link Code#UNKNOWN} if mapping does not exist. + */ + public Code getCode() { + return code; + } +} diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/TableResult.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/TableResult.java index 42044596bd..a7aa6ba9de 100644 --- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/TableResult.java +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/TableResult.java @@ -47,6 +47,8 @@ public abstract static class Builder { public abstract TableResult.Builder setQueryId(String queryId); + public abstract TableResult.Builder setJobCreationReason(JobCreationReason jobCreationReason); + /** Creates a @code TableResult} object. */ public abstract TableResult build(); } @@ -76,6 +78,9 @@ public static Builder newBuilder() { @Nullable public abstract String getQueryId(); + @Nullable + public abstract JobCreationReason getJobCreationReason(); + @Override public boolean hasNextPage() { return getPageNoSchema().hasNextPage(); @@ -94,6 +99,7 @@ public TableResult getNextPage() { .setTotalRows(getTotalRows()) .setPageNoSchema(getPageNoSchema().getNextPage()) .setQueryId(getQueryId()) + .setJobCreationReason(getJobCreationReason()) .build(); } return null; diff --git a/google-cloud-bigquery/src/test/java/MetadataCacheStatsTest.java b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/MetadataCacheStatsTest.java similarity index 100% rename from google-cloud-bigquery/src/test/java/MetadataCacheStatsTest.java rename to google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/MetadataCacheStatsTest.java diff --git a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java index 806a59292b..39d7ed2a0d 100644 --- a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java +++ b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -93,6 +94,7 @@ import com.google.cloud.bigquery.InsertAllResponse; import com.google.cloud.bigquery.Job; import com.google.cloud.bigquery.JobConfiguration; +import com.google.cloud.bigquery.JobCreationReason; import com.google.cloud.bigquery.JobId; import com.google.cloud.bigquery.JobInfo; import com.google.cloud.bigquery.JobStatistics; @@ -7316,14 +7318,16 @@ void testTableResultJobIdAndQueryId() throws InterruptedException { String query = "SELECT 1 as one"; QueryJobConfiguration configStateless = QueryJobConfiguration.newBuilder(query).build(); TableResult result = bigQuery.query(configStateless); - // A stateless query should result in either a queryId (stateless success) or a jobId (fallback - // to a job). - // Exactly one of them should be non-null. - // Ideally Stateless query will return queryId but in some cases it would return jobId instead - // of queryId based on the query complexity or other factors (job timeout configs). - assertTrue( - (result.getJobId() != null) ^ (result.getQueryId() != null), - "Exactly one of jobId or queryId should be non-null"); + // This should trigger a stateless query due to the query simplicity. However, BQ's engine + // may configure this to be a job due a variety of factors. The QueryID is autopopulated and + // may also return a JobId if changed to a job. For the query above, the Job Creation Reason + // would always be `OTHER` as it is not request, a large result, or due to a timeout. + assertNotNull(result.getQueryId()); + if (result.getJobCreationReason() != null) { + assertNotNull(result.getJobId()); + assertEquals(result.getQueryId(), result.getJobId().getJob()); + assertEquals(JobCreationReason.Code.OTHER, result.getJobCreationReason().getCode()); + } // Test scenario 2 by failing stateless check by setting job timeout. QueryJobConfiguration configQueryWithJob = @@ -7416,9 +7420,18 @@ void testQueryWithTimeout() throws InterruptedException { // Stateless query returns TableResult QueryJobConfiguration config = QueryJobConfiguration.newBuilder(query).build(); Object result = bigQuery.queryWithTimeout(config, null, null); - assertTrue(result instanceof TableResult); - assertNull(((TableResult) result).getJobId()); - assertNotNull(((TableResult) result).getQueryId()); + assertInstanceOf(TableResult.class, result); + // This should trigger a stateless query due to the query simplicity. However, BQ's engine + // may configure this to be a job due a variety of factors. The QueryID is autopopulated and + // may also return a JobId if changed to a Job. For the query above, the Job Creation Reason + // would always be `OTHER` as it is not request, a large result, or due to a timeout. + TableResult tableResult = (TableResult) result; + assertNotNull(tableResult.getQueryId()); + if (tableResult.getJobCreationReason() != null) { + assertNotNull(tableResult.getJobId()); + assertEquals(tableResult.getQueryId(), tableResult.getJobId().getJob()); + assertEquals(JobCreationReason.Code.OTHER, tableResult.getJobCreationReason().getCode()); + } // Stateful query returns Job // Test scenario 2 to ensure job is created if JobCreationMode is set, but for a small query