Skip to content

Commit 6dcc900

Browse files
authored
feat: Introduce DataFormatOptions to configure the output of BigQuery data types (#4010)
* feat: Create DataFormatOptions in BigQuery * feat: Add Builder class for DataFormatOptions * fix: Update existing references of useInt64Timestamp to use DataFormatOption's variant * chore: Fix lint issues * chore: Address PR feedback * chore: Add tests for useInt64Timestamp behavior * chore: Address failing tests and GCA * chore: Remove unused fromPb method
1 parent a942b07 commit 6dcc900

File tree

7 files changed

+230
-17
lines changed

7 files changed

+230
-17
lines changed

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,7 +1731,10 @@ public TableDataList call() throws IOException {
17311731
new PageImpl<>(
17321732
new TableDataPageFetcher(tableId, schema, serviceOptions, cursor, pageOptionMap),
17331733
cursor,
1734-
transformTableData(result.getRows(), schema, serviceOptions.getUseInt64Timestamps())),
1734+
transformTableData(
1735+
result.getRows(),
1736+
schema,
1737+
serviceOptions.getDataFormatOptions().useInt64Timestamp())),
17351738
result.getTotalRows());
17361739
} catch (BigQueryRetryHelperException e) {
17371740
throw BigQueryException.translateAndThrow(e);
@@ -2007,7 +2010,9 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20072010
new QueryPageFetcher(jobId, schema, getOptions(), cursor, optionMap(options)),
20082011
cursor,
20092012
transformTableData(
2010-
results.getRows(), schema, getOptions().getUseInt64Timestamps())))
2013+
results.getRows(),
2014+
schema,
2015+
getOptions().getDataFormatOptions().useInt64Timestamp())))
20112016
.setJobId(jobId)
20122017
.setQueryId(results.getQueryId())
20132018
.build();
@@ -2021,7 +2026,9 @@ public com.google.api.services.bigquery.model.QueryResponse call()
20212026
new TableDataPageFetcher(null, schema, getOptions(), null, optionMap(options)),
20222027
null,
20232028
transformTableData(
2024-
results.getRows(), schema, getOptions().getUseInt64Timestamps())))
2029+
results.getRows(),
2030+
schema,
2031+
getOptions().getDataFormatOptions().useInt64Timestamp())))
20252032
// Return the JobID of the successful job
20262033
.setJobId(
20272034
results.getJobReference() != null ? JobId.fromPb(results.getJobReference()) : null)
@@ -2066,10 +2073,9 @@ && getOptions().getOpenTelemetryTracer() != null) {
20662073
}
20672074
try (Scope queryScope = querySpan != null ? querySpan.makeCurrent() : null) {
20682075
// If all parameters passed in configuration are supported by the query() method on the
2069-
// backend,
2070-
// put on fast path
2076+
// backend, put on fast path
20712077
QueryRequestInfo requestInfo =
2072-
new QueryRequestInfo(configuration, getOptions().getUseInt64Timestamps());
2078+
new QueryRequestInfo(configuration, getOptions().getDataFormatOptions());
20732079
if (requestInfo.isFastQuerySupported(jobId)) {
20742080
// Be careful when setting the projectID in JobId, if a projectID is specified in the JobId,
20752081
// the job created by the query method will use that project. This may cause the query to

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

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.bigquery;
1818

1919
import com.google.api.core.BetaApi;
20+
import com.google.api.core.ObsoleteApi;
2021
import com.google.api.gax.retrying.ResultRetryAlgorithm;
2122
import com.google.cloud.ServiceDefaults;
2223
import com.google.cloud.ServiceOptions;
@@ -26,6 +27,7 @@
2627
import com.google.cloud.bigquery.spi.BigQueryRpcFactory;
2728
import com.google.cloud.bigquery.spi.v2.HttpBigQueryRpc;
2829
import com.google.cloud.http.HttpTransportOptions;
30+
import com.google.common.base.Preconditions;
2931
import com.google.common.collect.ImmutableSet;
3032
import io.opentelemetry.api.trace.Tracer;
3133
import java.util.Set;
@@ -41,6 +43,7 @@ public class BigQueryOptions extends ServiceOptions<BigQuery, BigQueryOptions> {
4143
// set the option ThrowNotFound when you want to throw the exception when the value not found
4244
private boolean setThrowNotFound;
4345
private boolean useInt64Timestamps;
46+
private DataFormatOptions dataFormatOptions;
4447
private JobCreationMode defaultJobCreationMode = JobCreationMode.JOB_CREATION_MODE_UNSPECIFIED;
4548
private boolean enableOpenTelemetryTracing;
4649
private Tracer openTelemetryTracer;
@@ -70,6 +73,7 @@ public static class Builder extends ServiceOptions.Builder<BigQuery, BigQueryOpt
7073

7174
private String location;
7275
private boolean useInt64Timestamps;
76+
private DataFormatOptions dataFormatOptions;
7377
private boolean enableOpenTelemetryTracing;
7478
private Tracer openTelemetryTracer;
7579
private ResultRetryAlgorithm<?> resultRetryAlgorithm;
@@ -94,11 +98,32 @@ public Builder setLocation(String location) {
9498
return this;
9599
}
96100

101+
/**
102+
* This setter is marked as Obsolete. Prefer {@link #setDataFormatOptions(DataFormatOptions)} to
103+
* set the int64timestamp configuration instead.
104+
*
105+
* <p>If useInt64Timestamps value is set in here and via DataFormatOptions, the
106+
* DataFormatOptions configuration value is used.
107+
*
108+
* <p>{@code DataFormatOptions.newBuilder().setUseInt64Timestamp(...).build()}
109+
*/
110+
@ObsoleteApi("Use setDataFormatOptions(DataFormatOptions) instead")
97111
public Builder setUseInt64Timestamps(boolean useInt64Timestamps) {
98112
this.useInt64Timestamps = useInt64Timestamps;
99113
return this;
100114
}
101115

116+
/**
117+
* Set the format options for the BigQuery data types
118+
*
119+
* @param dataFormatOptions Configuration of the formatting options
120+
*/
121+
public Builder setDataFormatOptions(DataFormatOptions dataFormatOptions) {
122+
Preconditions.checkNotNull(dataFormatOptions, "DataFormatOptions cannot be null");
123+
this.dataFormatOptions = dataFormatOptions;
124+
return this;
125+
}
126+
102127
/**
103128
* Enables OpenTelemetry tracing functionality for this BigQuery instance
104129
*
@@ -143,6 +168,15 @@ private BigQueryOptions(Builder builder) {
143168
} else {
144169
this.resultRetryAlgorithm = BigQueryBaseService.DEFAULT_BIGQUERY_EXCEPTION_HANDLER;
145170
}
171+
172+
// If dataFormatOptions is not set, then create a new instance and set it with the
173+
// useInt64Timestamps configured in BigQueryOptions
174+
if (builder.dataFormatOptions == null) {
175+
this.dataFormatOptions =
176+
DataFormatOptions.newBuilder().useInt64Timestamp(builder.useInt64Timestamps).build();
177+
} else {
178+
this.dataFormatOptions = builder.dataFormatOptions;
179+
}
146180
}
147181

148182
private static class BigQueryDefaults implements ServiceDefaults<BigQuery, BigQueryOptions> {
@@ -191,8 +225,23 @@ public void setThrowNotFound(boolean setThrowNotFound) {
191225
this.setThrowNotFound = setThrowNotFound;
192226
}
193227

228+
/**
229+
* This setter is marked as Obsolete. Prefer {@link
230+
* Builder#setDataFormatOptions(DataFormatOptions)} to set the int64timestamp configuration
231+
* instead.
232+
*
233+
* <p>If useInt64Timestamps is set via DataFormatOptions, then the value in DataFormatOptions will
234+
* be used. Otherwise, this value will be passed to DataFormatOptions.
235+
*
236+
* <p>Alternative: {@code DataFormatOptions.newBuilder().setUseInt64Timestamp(...).build()}
237+
*/
238+
@ObsoleteApi("Use Builder#setDataFormatOptions(DataFormatOptions) instead")
194239
public void setUseInt64Timestamps(boolean useInt64Timestamps) {
195240
this.useInt64Timestamps = useInt64Timestamps;
241+
// Because this setter exists outside the Builder, DataFormatOptions needs be rebuilt to
242+
// account for this setting.
243+
this.dataFormatOptions =
244+
dataFormatOptions.toBuilder().useInt64Timestamp(useInt64Timestamps).build();
196245
}
197246

198247
@Deprecated
@@ -206,8 +255,22 @@ public boolean getThrowNotFound() {
206255
return setThrowNotFound;
207256
}
208257

258+
/**
259+
* This getter is marked as Obsolete. Prefer {@link
260+
* DataFormatOptions.Builder#useInt64Timestamp(boolean)} to set the int64timestamp configuration
261+
* instead.
262+
*
263+
* <p>Warning: DataFormatOptions values have precedence. Use {@link
264+
* DataFormatOptions#useInt64Timestamp()} to get `useInt64Timestamp` value used by the BigQuery
265+
* client.
266+
*/
267+
@ObsoleteApi("Use getDataFormatOptions().isUseInt64Timestamp() instead")
209268
public boolean getUseInt64Timestamps() {
210-
return useInt64Timestamps;
269+
return dataFormatOptions.useInt64Timestamp();
270+
}
271+
272+
public DataFormatOptions getDataFormatOptions() {
273+
return dataFormatOptions;
211274
}
212275

213276
public JobCreationMode getDefaultJobCreationMode() {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigquery;
17+
18+
import com.google.auto.value.AutoValue;
19+
import java.io.Serializable;
20+
21+
/**
22+
* Google BigQuery DataFormatOptions. Configures the output format for data types returned from
23+
* BigQuery.
24+
*/
25+
@AutoValue
26+
public abstract class DataFormatOptions implements Serializable {
27+
public enum TimestampFormatOptions {
28+
TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED("TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED"),
29+
FLOAT64("FLOAT64"),
30+
INT64("INT64"),
31+
ISO8601_STRING("ISO8601_STRING");
32+
33+
private final String format;
34+
35+
TimestampFormatOptions(String format) {
36+
this.format = format;
37+
}
38+
39+
@Override
40+
public String toString() {
41+
return format;
42+
}
43+
}
44+
45+
public abstract boolean useInt64Timestamp();
46+
47+
public abstract TimestampFormatOptions timestampFormatOptions();
48+
49+
public static Builder newBuilder() {
50+
return new AutoValue_DataFormatOptions.Builder()
51+
.useInt64Timestamp(false)
52+
.timestampFormatOptions(TimestampFormatOptions.TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED);
53+
}
54+
55+
public abstract Builder toBuilder();
56+
57+
@AutoValue.Builder
58+
public abstract static class Builder {
59+
public abstract Builder useInt64Timestamp(boolean useInt64Timestamp);
60+
61+
public abstract Builder timestampFormatOptions(TimestampFormatOptions timestampFormatOptions);
62+
63+
public abstract DataFormatOptions build();
64+
}
65+
66+
com.google.api.services.bigquery.model.DataFormatOptions toPb() {
67+
com.google.api.services.bigquery.model.DataFormatOptions request =
68+
new com.google.api.services.bigquery.model.DataFormatOptions();
69+
request.setUseInt64Timestamp(useInt64Timestamp());
70+
request.setTimestampOutputFormat(timestampFormatOptions().toString());
71+
return request;
72+
}
73+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ final class QueryRequestInfo {
4646
private final DataFormatOptions formatOptions;
4747
private final String reservation;
4848

49-
QueryRequestInfo(QueryJobConfiguration config, Boolean useInt64Timestamps) {
49+
QueryRequestInfo(
50+
QueryJobConfiguration config, com.google.cloud.bigquery.DataFormatOptions dataFormatOptions) {
5051
this.config = config;
5152
this.connectionProperties = config.getConnectionProperties();
5253
this.defaultDataset = config.getDefaultDataset();
@@ -61,7 +62,7 @@ final class QueryRequestInfo {
6162
this.useLegacySql = config.useLegacySql();
6263
this.useQueryCache = config.useQueryCache();
6364
this.jobCreationMode = config.getJobCreationMode();
64-
this.formatOptions = new DataFormatOptions().setUseInt64Timestamp(useInt64Timestamps);
65+
this.formatOptions = dataFormatOptions.toPb();
6566
this.reservation = config.getReservation();
6667
}
6768

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
package com.google.cloud.bigquery;
1818

19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertTrue;
23+
1924
import com.google.cloud.TransportOptions;
2025
import org.junit.Assert;
2126
import org.junit.Test;
@@ -35,4 +40,55 @@ public void testInvalidTransport() {
3540
Assert.assertNotNull(expected.getMessage());
3641
}
3742
}
43+
44+
@Test
45+
public void dataFormatOptions_createdByDefault() {
46+
BigQueryOptions options = BigQueryOptions.newBuilder().setProjectId("project-id").build();
47+
48+
assertNotNull(options.getDataFormatOptions());
49+
assertFalse(options.getDataFormatOptions().useInt64Timestamp());
50+
assertEquals(
51+
DataFormatOptions.TimestampFormatOptions.TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED,
52+
options.getDataFormatOptions().timestampFormatOptions());
53+
}
54+
55+
@Test
56+
public void nonBuilderSetUseInt64Timestamp_capturedInDataFormatOptions() {
57+
BigQueryOptions options =
58+
BigQueryOptions.newBuilder()
59+
.setDataFormatOptions(DataFormatOptions.newBuilder().useInt64Timestamp(false).build())
60+
.setProjectId("project-id")
61+
.build();
62+
options.setUseInt64Timestamps(true);
63+
64+
assertTrue(options.getDataFormatOptions().useInt64Timestamp());
65+
}
66+
67+
@Test
68+
public void nonBuilderSetUseInt64Timestamp_overridesEverything() {
69+
BigQueryOptions options = BigQueryOptions.newBuilder().setProjectId("project-id").build();
70+
options.setUseInt64Timestamps(true);
71+
72+
assertTrue(options.getDataFormatOptions().useInt64Timestamp());
73+
}
74+
75+
@Test
76+
public void noDataFormatOptions_capturesUseInt64TimestampSetInBuilder() {
77+
BigQueryOptions options =
78+
BigQueryOptions.newBuilder().setUseInt64Timestamps(true).setProjectId("project-id").build();
79+
80+
assertTrue(options.getDataFormatOptions().useInt64Timestamp());
81+
}
82+
83+
@Test
84+
public void dataFormatOptionsSetterHasPrecedence() {
85+
BigQueryOptions options =
86+
BigQueryOptions.newBuilder()
87+
.setProjectId("project-id")
88+
.setDataFormatOptions(DataFormatOptions.newBuilder().useInt64Timestamp(true).build())
89+
.setUseInt64Timestamps(false)
90+
.build();
91+
92+
assertTrue(options.getDataFormatOptions().useInt64Timestamp());
93+
}
3894
}

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ public class QueryRequestInfoTest {
140140
.setJobCreationMode(jobCreationModeRequired)
141141
.setReservation(RESERVATION)
142142
.build();
143-
QueryRequestInfo REQUEST_INFO = new QueryRequestInfo(QUERY_JOB_CONFIGURATION, false);
143+
QueryRequestInfo REQUEST_INFO =
144+
new QueryRequestInfo(QUERY_JOB_CONFIGURATION, DataFormatOptions.newBuilder().build());
144145
private static final QueryJobConfiguration QUERY_JOB_CONFIGURATION_SUPPORTED =
145146
QueryJobConfiguration.newBuilder(QUERY)
146147
.setUseQueryCache(USE_QUERY_CACHE)
@@ -156,7 +157,8 @@ public class QueryRequestInfoTest {
156157
.setReservation(RESERVATION)
157158
.build();
158159
QueryRequestInfo REQUEST_INFO_SUPPORTED =
159-
new QueryRequestInfo(QUERY_JOB_CONFIGURATION_SUPPORTED, false);
160+
new QueryRequestInfo(
161+
QUERY_JOB_CONFIGURATION_SUPPORTED, DataFormatOptions.newBuilder().build());
160162

161163
@Test
162164
public void testIsFastQuerySupported() {
@@ -177,17 +179,25 @@ public void testToPb() {
177179
@Test
178180
public void equalTo() {
179181
compareQueryRequestInfo(
180-
new QueryRequestInfo(QUERY_JOB_CONFIGURATION_SUPPORTED, false), REQUEST_INFO_SUPPORTED);
181-
compareQueryRequestInfo(new QueryRequestInfo(QUERY_JOB_CONFIGURATION, false), REQUEST_INFO);
182+
new QueryRequestInfo(
183+
QUERY_JOB_CONFIGURATION_SUPPORTED, DataFormatOptions.newBuilder().build()),
184+
REQUEST_INFO_SUPPORTED);
185+
compareQueryRequestInfo(
186+
new QueryRequestInfo(QUERY_JOB_CONFIGURATION, DataFormatOptions.newBuilder().build()),
187+
REQUEST_INFO);
182188
}
183189

184190
@Test
185191
public void testInt64Timestamp() {
186-
QueryRequestInfo requestInfo = new QueryRequestInfo(QUERY_JOB_CONFIGURATION, false);
192+
QueryRequestInfo requestInfo =
193+
new QueryRequestInfo(QUERY_JOB_CONFIGURATION, DataFormatOptions.newBuilder().build());
187194
QueryRequest requestPb = requestInfo.toPb();
188195
assertFalse(requestPb.getFormatOptions().getUseInt64Timestamp());
189196

190-
QueryRequestInfo requestInfoLosslessTs = new QueryRequestInfo(QUERY_JOB_CONFIGURATION, true);
197+
QueryRequestInfo requestInfoLosslessTs =
198+
new QueryRequestInfo(
199+
QUERY_JOB_CONFIGURATION,
200+
DataFormatOptions.newBuilder().useInt64Timestamp(true).build());
191201
QueryRequest requestLosslessTsPb = requestInfoLosslessTs.toPb();
192202
assertTrue(requestLosslessTsPb.getFormatOptions().getUseInt64Timestamp());
193203
}

0 commit comments

Comments
 (0)