Note: Only applies when {@link QueryResultsFormat#ARROW} is enabled. For Arrow result + * streams, this precision setting governs binary Arrow timestamp column types and takes + * precedence over {@link DataFormatOptions.TimestampFormatOptions}, which applies to default + * {@link QueryResultsFormat#STRUCT_ENCODING} JSON results. + */ + @BetaApi + public TimestampPrecision getPicosTimestampPrecision() { + return picosTimestampPrecision; + } + + /** [Beta] Returns a new builder for {@link ArrowSerializationOptions}. */ + @BetaApi + public static Builder newBuilder() { + return new Builder(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bufferCompression", bufferCompression) + .add("picosTimestampPrecision", picosTimestampPrecision) + .toString(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArrowSerializationOptions that = (ArrowSerializationOptions) o; + return bufferCompression == that.bufferCompression + && picosTimestampPrecision == that.picosTimestampPrecision; + } + + @Override + public int hashCode() { + return Objects.hash(bufferCompression, picosTimestampPrecision); + } + + com.google.api.services.bigquery.model.ArrowSerializationOptions toPb() { + return ArrowSerializationOptionsConverter.toPb(this); + } + + static ArrowSerializationOptions fromPb( + com.google.api.services.bigquery.model.ArrowSerializationOptions optionsPb) { + return ArrowSerializationOptionsConverter.fromPb(optionsPb); + } + + /** [Beta] Builder for {@link ArrowSerializationOptions}. */ + @BetaApi + public static final class Builder { + private CompressionCodec bufferCompression = CompressionCodec.UNCOMPRESSED; + private TimestampPrecision picosTimestampPrecision = TimestampPrecision.MICROS; + + private Builder() {} + + /** + * [Beta] Sets the buffer compression algorithm (e.g., LZ4_FRAME, ZSTD, UNCOMPRESSED). + */ + @BetaApi + public Builder setBufferCompression(CompressionCodec bufferCompression) { + this.bufferCompression = checkNotNull(bufferCompression, "bufferCompression cannot be null"); + return this; + } + + /** + * [Beta] Sets the timestamp precision for Arrow timestamp types. + * + *
Note: Only applies when {@link QueryResultsFormat#ARROW} is enabled. For Arrow result + * streams, this precision setting governs binary Arrow timestamp column types and takes + * precedence over {@link DataFormatOptions.TimestampFormatOptions}, which applies to default + * {@link QueryResultsFormat#STRUCT_ENCODING} JSON results. + */ + @BetaApi + public Builder setPicosTimestampPrecision(TimestampPrecision picosTimestampPrecision) { + this.picosTimestampPrecision = + checkNotNull(picosTimestampPrecision, "picosTimestampPrecision cannot be null"); + return this; + } + + /** [Beta] Builds a new instance of {@link ArrowSerializationOptions}. */ + @BetaApi + public ArrowSerializationOptions build() { + return new ArrowSerializationOptions(this); + } + } +} diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/ArrowSerializationOptionsConverter.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/ArrowSerializationOptionsConverter.java new file mode 100644 index 000000000000..07ef882ae9a1 --- /dev/null +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/ArrowSerializationOptionsConverter.java @@ -0,0 +1,66 @@ +/* + * 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 org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +final class ArrowSerializationOptionsConverter { + + private ArrowSerializationOptionsConverter() {} + + static com.google.api.services.bigquery.model.@Nullable ArrowSerializationOptions toPb( + @Nullable ArrowSerializationOptions options) { + if (options == null) { + return null; + } + com.google.api.services.bigquery.model.ArrowSerializationOptions optionsPb = + new com.google.api.services.bigquery.model.ArrowSerializationOptions(); + optionsPb.setBufferCompression(options.getBufferCompression().getValue()); + optionsPb.setPicosTimestampPrecision(options.getPicosTimestampPrecision().getValue()); + return optionsPb; + } + + static @Nullable ArrowSerializationOptions fromPb(@Nullable Object optionsPbObj) { + if (optionsPbObj == null) { + return null; + } + com.google.api.services.bigquery.model.ArrowSerializationOptions optionsPb = + (com.google.api.services.bigquery.model.ArrowSerializationOptions) optionsPbObj; + ArrowSerializationOptions.Builder builder = ArrowSerializationOptions.newBuilder(); + if (optionsPb.getBufferCompression() != null) { + for (ArrowSerializationOptions.CompressionCodec codec : + ArrowSerializationOptions.CompressionCodec.values()) { + if (codec.getValue().equalsIgnoreCase(optionsPb.getBufferCompression())) { + builder.setBufferCompression(codec); + break; + } + } + } + if (optionsPb.getPicosTimestampPrecision() != null) { + for (ArrowSerializationOptions.TimestampPrecision precision : + ArrowSerializationOptions.TimestampPrecision.values()) { + if (precision.getValue().equalsIgnoreCase(optionsPb.getPicosTimestampPrecision())) { + builder.setPicosTimestampPrecision(precision); + break; + } + } + } + return builder.build(); + } +} diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryJobConfiguration.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryJobConfiguration.java index a62fbb5008d4..8fc78b3edc73 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryJobConfiguration.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryJobConfiguration.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; +import com.google.api.core.BetaApi; import com.google.api.services.bigquery.model.JobConfigurationQuery; import com.google.api.services.bigquery.model.QueryParameter; import com.google.cloud.bigquery.JobInfo.CreateDisposition; @@ -75,6 +76,8 @@ public final class QueryJobConfiguration extends JobConfiguration { private final Long maxResults; private final JobCreationMode jobCreationMode; private final String reservation; + private final QueryResultsFormat queryResultsFormat; + private final ArrowSerializationOptions arrowSerializationOptions; /** * Priority levels for a query. If not specified the priority is assumed to be {@link @@ -144,6 +147,8 @@ public static final class Builder private Long maxResults; private JobCreationMode jobCreationMode; private String reservation; + private QueryResultsFormat queryResultsFormat = QueryResultsFormat.STRUCT_ENCODING; + private ArrowSerializationOptions arrowSerializationOptions; private Builder() { super(Type.QUERY); @@ -181,6 +186,8 @@ private Builder(QueryJobConfiguration jobConfiguration) { this.maxResults = jobConfiguration.maxResults; this.jobCreationMode = jobConfiguration.jobCreationMode; this.reservation = jobConfiguration.reservation; + this.queryResultsFormat = jobConfiguration.queryResultsFormat; + this.arrowSerializationOptions = jobConfiguration.arrowSerializationOptions; } private Builder(com.google.api.services.bigquery.model.JobConfiguration configurationPb) { @@ -701,6 +708,45 @@ public Builder setReservation(String reservation) { return this; } + /** + * [Beta] Sets the query results response format. Defaults to {@link + * QueryResultsFormat#STRUCT_ENCODING}. + * + *
When set to {@link QueryResultsFormat#ARROW}, query results are returned in binary Apache + * Arrow format, utilizing gRPC Storage Read streams for subsequent pages. + * + *
Prerequisite: Requires the BigQuery Storage Read API ({@code + * bigquerystorage.googleapis.com}) to be enabled on your GCP project. See the Google Cloud + * Enabling APIs Guide. + * + * @param queryResultsFormat the format for query result payloads + * @return the Builder + */ + @BetaApi + public Builder setQueryResultsFormat(QueryResultsFormat queryResultsFormat) { + this.queryResultsFormat = + checkNotNull(queryResultsFormat, "queryResultsFormat cannot be null"); + return this; + } + + /** + * [Beta] Sets Arrow serialization options. Defaults to null. + * + *
Note: Only applied in the request payload when {@code queryResultsFormat} is {@code + * ARROW}. + * + * @param arrowSerializationOptions the Arrow serialization options to set + * @return the Builder + */ + @BetaApi + public Builder setArrowSerializationOptions( + ArrowSerializationOptions arrowSerializationOptions) { + this.arrowSerializationOptions = + checkNotNull(arrowSerializationOptions, "arrowSerializationOptions cannot be null"); + return this; + } + public QueryJobConfiguration build() { return new QueryJobConfiguration(this); } @@ -747,6 +793,8 @@ private QueryJobConfiguration(Builder builder) { this.maxResults = builder.maxResults; this.jobCreationMode = builder.jobCreationMode; this.reservation = builder.reservation; + this.queryResultsFormat = builder.queryResultsFormat; + this.arrowSerializationOptions = builder.arrowSerializationOptions; } /** @@ -973,6 +1021,18 @@ public Builder toBuilder() { return new Builder(this); } + /** [Beta] Returns the query results response format. */ + @BetaApi + public QueryResultsFormat getQueryResultsFormat() { + return queryResultsFormat; + } + + /** [Beta] Returns Arrow serialization options, or null if unset. */ + @BetaApi + public ArrowSerializationOptions getArrowSerializationOptions() { + return arrowSerializationOptions; + } + @Override ToStringHelper toStringHelper() { return super.toStringHelper() @@ -1004,13 +1064,23 @@ ToStringHelper toStringHelper() { .add("rangePartitioning", rangePartitioning) .add("connectionProperties", connectionProperties) .add("jobCreationMode", jobCreationMode) - .add("reservation", reservation); + .add("reservation", reservation) + .add("queryResultsFormat", queryResultsFormat) + .add("arrowSerializationOptions", arrowSerializationOptions); } @Override public boolean equals(Object obj) { - return obj == this - || obj instanceof QueryJobConfiguration && baseEquals((QueryJobConfiguration) obj); + if (obj == this) { + return true; + } + if (obj == null || !(obj instanceof QueryJobConfiguration)) { + return false; + } + QueryJobConfiguration other = (QueryJobConfiguration) obj; + return baseEquals(other) + && Objects.equals(queryResultsFormat, other.queryResultsFormat) + && Objects.equals(arrowSerializationOptions, other.arrowSerializationOptions); } @Override @@ -1043,7 +1113,9 @@ public int hashCode() { labels, rangePartitioning, connectionProperties, - reservation); + reservation, + queryResultsFormat, + arrowSerializationOptions); } @Override diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryResultsFormat.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryResultsFormat.java new file mode 100644 index 000000000000..5507286e880d --- /dev/null +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryResultsFormat.java @@ -0,0 +1,31 @@ +/* + * 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 com.google.api.core.BetaApi; +import org.jspecify.annotations.NullMarked; + +/** [Beta] The format of the query results. */ +@BetaApi +@NullMarked +public enum QueryResultsFormat { + /** Serialized row data in Apache Arrow format. */ + ARROW, + + /** Default encoding of results as JSON struct array. */ + STRUCT_ENCODING +} diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryJobConfigurationTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryJobConfigurationTest.java index 7fe41daa0608..4c4f847e9a96 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryJobConfigurationTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/QueryJobConfigurationTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.cloud.bigquery.JobInfo.CreateDisposition; import com.google.cloud.bigquery.JobInfo.SchemaUpdateOption; @@ -241,6 +242,97 @@ public void testJobCreationMode() { QUERY_JOB_CONFIGURATION_SET_JOB_CREATION_MODE.toBuilder().build()); } + @Test + public void testArrowConfigurations() { + QueryResultsFormat format = QueryResultsFormat.ARROW; + ArrowSerializationOptions options = + ArrowSerializationOptions.newBuilder() + .setBufferCompression(ArrowSerializationOptions.CompressionCodec.LZ4_FRAME) + .setPicosTimestampPrecision(ArrowSerializationOptions.TimestampPrecision.NANOS) + .build(); + QueryJobConfiguration job = + QueryJobConfiguration.newBuilder(QUERY) + .setQueryResultsFormat(format) + .setArrowSerializationOptions(options) + .build(); + + assertEquals(format, job.getQueryResultsFormat()); + assertEquals(options, job.getArrowSerializationOptions()); + + // Test toBuilder + QueryJobConfiguration copiedJob = job.toBuilder().build(); + assertEquals(job, copiedJob); + assertEquals(format, copiedJob.getQueryResultsFormat()); + assertEquals(options, copiedJob.getArrowSerializationOptions()); + + // Test toPb/fromPb (not preserved) + QueryJobConfiguration jobFromPb = QueryJobConfiguration.fromPb(job.toPb()); + assertEquals(QueryResultsFormat.STRUCT_ENCODING, jobFromPb.getQueryResultsFormat()); + assertNull(jobFromPb.getArrowSerializationOptions()); + } + + @Test + public void testArrowSerializationOptionsNullChecks() { + ArrowSerializationOptions.Builder builder = ArrowSerializationOptions.newBuilder(); + assertEquals( + ArrowSerializationOptions.CompressionCodec.UNCOMPRESSED, + builder.build().getBufferCompression()); + assertEquals( + ArrowSerializationOptions.TimestampPrecision.MICROS, + builder.build().getPicosTimestampPrecision()); + + NullPointerException ex1 = + assertThrows(NullPointerException.class, () -> builder.setBufferCompression(null)); + assertEquals("bufferCompression cannot be null", ex1.getMessage()); + + NullPointerException ex2 = + assertThrows(NullPointerException.class, () -> builder.setPicosTimestampPrecision(null)); + assertEquals("picosTimestampPrecision cannot be null", ex2.getMessage()); + } + + @Test + public void testQueryJobConfigurationDefaults() { + QueryJobConfiguration defaultJob = QueryJobConfiguration.newBuilder(QUERY).build(); + + // Default query format is STRUCT_ENCODING; Arrow options are null by default + assertEquals(QueryResultsFormat.STRUCT_ENCODING, defaultJob.getQueryResultsFormat()); + assertNull(defaultJob.getArrowSerializationOptions()); + + // Verify toBuilder preserves defaults + QueryJobConfiguration copiedJob = defaultJob.toBuilder().build(); + assertEquals(QueryResultsFormat.STRUCT_ENCODING, copiedJob.getQueryResultsFormat()); + assertNull(copiedJob.getArrowSerializationOptions()); + } + + @Test + public void testArrowFormatWithNullSerializationOptions() { + QueryJobConfiguration job = + QueryJobConfiguration.newBuilder(QUERY) + .setQueryResultsFormat(QueryResultsFormat.ARROW) + .build(); + + assertEquals(QueryResultsFormat.ARROW, job.getQueryResultsFormat()); + assertNull(job.getArrowSerializationOptions()); + + // Verify toBuilder preserves ARROW format with null options + QueryJobConfiguration copiedJob = job.toBuilder().build(); + assertEquals(QueryResultsFormat.ARROW, copiedJob.getQueryResultsFormat()); + assertNull(copiedJob.getArrowSerializationOptions()); + } + + @Test + public void testQueryJobConfigurationArrowNullChecks() { + QueryJobConfiguration.Builder builder = QueryJobConfiguration.newBuilder(QUERY); + + NullPointerException ex1 = + assertThrows(NullPointerException.class, () -> builder.setQueryResultsFormat(null)); + assertEquals("queryResultsFormat cannot be null", ex1.getMessage()); + + NullPointerException ex2 = + assertThrows(NullPointerException.class, () -> builder.setArrowSerializationOptions(null)); + assertEquals("arrowSerializationOptions cannot be null", ex2.getMessage()); + } + private void compareQueryJobConfiguration( QueryJobConfiguration expected, QueryJobConfiguration value) { assertEquals(expected, value); @@ -275,5 +367,7 @@ private void compareQueryJobConfiguration( assertEquals(expected.getPositionalParameters(), value.getPositionalParameters()); assertEquals(expected.getNamedParameters(), value.getNamedParameters()); assertEquals(expected.getReservation(), value.getReservation()); + assertEquals(expected.getQueryResultsFormat(), value.getQueryResultsFormat()); + assertEquals(expected.getArrowSerializationOptions(), value.getArrowSerializationOptions()); } }