If useInt64Timestamps value is set in here and via DataFormatOptions, the + * DataFormatOptions configuration value is used. + * + *
{@code DataFormatOptions.newBuilder().setUseInt64Timestamp(...).build()}
+ */
+ @ObsoleteApi("Use setDataFormatOptions(DataFormatOptions) instead")
public Builder setUseInt64Timestamps(boolean useInt64Timestamps) {
this.useInt64Timestamps = useInt64Timestamps;
return this;
}
+ /**
+ * Set the format options for the BigQuery data types
+ *
+ * @param dataFormatOptions Configuration of the formatting options
+ */
+ public Builder setDataFormatOptions(DataFormatOptions dataFormatOptions) {
+ Preconditions.checkNotNull(dataFormatOptions, "DataFormatOptions cannot be null");
+ this.dataFormatOptions = dataFormatOptions;
+ return this;
+ }
+
/**
* Enables OpenTelemetry tracing functionality for this BigQuery instance
*
@@ -143,6 +168,15 @@ private BigQueryOptions(Builder builder) {
} else {
this.resultRetryAlgorithm = BigQueryBaseService.DEFAULT_BIGQUERY_EXCEPTION_HANDLER;
}
+
+ // If dataFormatOptions is not set, then create a new instance and set it with the
+ // useInt64Timestamps configured in BigQueryOptions
+ if (builder.dataFormatOptions == null) {
+ this.dataFormatOptions =
+ DataFormatOptions.newBuilder().useInt64Timestamp(builder.useInt64Timestamps).build();
+ } else {
+ this.dataFormatOptions = builder.dataFormatOptions;
+ }
}
private static class BigQueryDefaults implements ServiceDefaults If useInt64Timestamps is set via DataFormatOptions, then the value in DataFormatOptions will
+ * be used. Otherwise, this value will be passed to DataFormatOptions.
+ *
+ * Alternative: {@code DataFormatOptions.newBuilder().setUseInt64Timestamp(...).build()}
+ */
+ @ObsoleteApi("Use Builder#setDataFormatOptions(DataFormatOptions) instead")
public void setUseInt64Timestamps(boolean useInt64Timestamps) {
this.useInt64Timestamps = useInt64Timestamps;
+ // Because this setter exists outside the Builder, DataFormatOptions needs be rebuilt to
+ // account for this setting.
+ this.dataFormatOptions =
+ dataFormatOptions.toBuilder().useInt64Timestamp(useInt64Timestamps).build();
}
@Deprecated
@@ -206,8 +255,22 @@ public boolean getThrowNotFound() {
return setThrowNotFound;
}
+ /**
+ * This getter is marked as Obsolete. Prefer {@link
+ * DataFormatOptions.Builder#useInt64Timestamp(boolean)} to set the int64timestamp configuration
+ * instead.
+ *
+ * Warning: DataFormatOptions values have precedence. Use {@link
+ * DataFormatOptions#useInt64Timestamp()} to get `useInt64Timestamp` value used by the BigQuery
+ * client.
+ */
+ @ObsoleteApi("Use getDataFormatOptions().isUseInt64Timestamp() instead")
public boolean getUseInt64Timestamps() {
- return useInt64Timestamps;
+ return dataFormatOptions.useInt64Timestamp();
+ }
+
+ public DataFormatOptions getDataFormatOptions() {
+ return dataFormatOptions;
}
public JobCreationMode getDefaultJobCreationMode() {
diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/DataFormatOptions.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/DataFormatOptions.java
new file mode 100644
index 000000000..beaadf32c
--- /dev/null
+++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/DataFormatOptions.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 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.auto.value.AutoValue;
+import java.io.Serializable;
+
+/**
+ * Google BigQuery DataFormatOptions. Configures the output format for data types returned from
+ * BigQuery.
+ */
+@AutoValue
+public abstract class DataFormatOptions implements Serializable {
+ public enum TimestampFormatOptions {
+ TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED("TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED"),
+ FLOAT64("FLOAT64"),
+ INT64("INT64"),
+ ISO8601_STRING("ISO8601_STRING");
+
+ private final String format;
+
+ TimestampFormatOptions(String format) {
+ this.format = format;
+ }
+
+ @Override
+ public String toString() {
+ return format;
+ }
+ }
+
+ public abstract boolean useInt64Timestamp();
+
+ public abstract TimestampFormatOptions timestampFormatOptions();
+
+ public static Builder newBuilder() {
+ return new AutoValue_DataFormatOptions.Builder()
+ .useInt64Timestamp(false)
+ .timestampFormatOptions(TimestampFormatOptions.TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED);
+ }
+
+ public abstract Builder toBuilder();
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder useInt64Timestamp(boolean useInt64Timestamp);
+
+ public abstract Builder timestampFormatOptions(TimestampFormatOptions timestampFormatOptions);
+
+ public abstract DataFormatOptions build();
+ }
+
+ com.google.api.services.bigquery.model.DataFormatOptions toPb() {
+ com.google.api.services.bigquery.model.DataFormatOptions request =
+ new com.google.api.services.bigquery.model.DataFormatOptions();
+ request.setUseInt64Timestamp(useInt64Timestamp());
+ request.setTimestampOutputFormat(timestampFormatOptions().toString());
+ return request;
+ }
+}
diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Field.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Field.java
index 3c959a73f..88e09c5c4 100644
--- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Field.java
+++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/Field.java
@@ -25,6 +25,7 @@
import com.google.api.services.bigquery.model.TableFieldSchema;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.io.Serializable;
import java.util.List;
@@ -62,6 +63,7 @@ public TableFieldSchema apply(Field field) {
private final Long maxLength;
private final Long scale;
private final Long precision;
+ private final Long timestampPrecision;
private final String defaultValueExpression;
private final String collation;
private final FieldElementType rangeElementType;
@@ -88,6 +90,7 @@ public static final class Builder {
private Long maxLength;
private Long scale;
private Long precision;
+ private Long timestampPrecision;
private String defaultValueExpression;
private String collation;
private FieldElementType rangeElementType;
@@ -104,6 +107,7 @@ private Builder(Field field) {
this.maxLength = field.maxLength;
this.scale = field.scale;
this.precision = field.precision;
+ this.timestampPrecision = field.timestampPrecision;
this.defaultValueExpression = field.defaultValueExpression;
this.collation = field.collation;
this.rangeElementType = field.rangeElementType;
@@ -254,6 +258,19 @@ public Builder setPrecision(Long precision) {
return this;
}
+ /**
+ * Specifies the precision for TIMESTAMP types.
+ *
+ * The default value is 6. Possible values are 6 (microsecond) or 12 (picosecond).
+ */
+ public Builder setTimestampPrecision(Long timestampPrecision) {
+ Preconditions.checkArgument(
+ timestampPrecision == 6L || timestampPrecision == 12L,
+ "Timestamp Precision must be 6 (microsecond) or 12 (picosecond)");
+ this.timestampPrecision = timestampPrecision;
+ return this;
+ }
+
/**
* DefaultValueExpression is used to specify the default value of a field using a SQL
* expression. It can only be set for top level fields (columns).
@@ -317,6 +334,7 @@ private Field(Builder builder) {
this.maxLength = builder.maxLength;
this.scale = builder.scale;
this.precision = builder.precision;
+ this.timestampPrecision = builder.timestampPrecision;
this.defaultValueExpression = builder.defaultValueExpression;
this.collation = builder.collation;
this.rangeElementType = builder.rangeElementType;
@@ -370,6 +388,11 @@ public Long getPrecision() {
return precision;
}
+ /** Returns the precision for TIMESTAMP type. */
+ public Long getTimestampPrecision() {
+ return timestampPrecision;
+ }
+
/** Return the default value of the field. */
public String getDefaultValueExpression() {
return defaultValueExpression;
@@ -408,6 +431,7 @@ public String toString() {
.add("maxLength", maxLength)
.add("scale", scale)
.add("precision", precision)
+ .add("timestampPrecision", timestampPrecision)
.add("defaultValueExpression", defaultValueExpression)
.add("collation", collation)
.add("rangeElementType", rangeElementType)
@@ -416,7 +440,19 @@ public String toString() {
@Override
public int hashCode() {
- return Objects.hash(name, type, mode, description, policyTags, rangeElementType);
+ return Objects.hash(
+ name,
+ type,
+ mode,
+ description,
+ policyTags,
+ maxLength,
+ scale,
+ precision,
+ timestampPrecision,
+ defaultValueExpression,
+ collation,
+ rangeElementType);
}
@Override
@@ -490,6 +526,9 @@ TableFieldSchema toPb() {
if (precision != null) {
fieldSchemaPb.setPrecision(precision);
}
+ if (timestampPrecision != null) {
+ fieldSchemaPb.setTimestampPrecision(timestampPrecision);
+ }
if (defaultValueExpression != null) {
fieldSchemaPb.setDefaultValueExpression(defaultValueExpression);
}
@@ -527,6 +566,9 @@ static Field fromPb(TableFieldSchema fieldSchemaPb) {
if (fieldSchemaPb.getPrecision() != null) {
fieldBuilder.setPrecision(fieldSchemaPb.getPrecision());
}
+ if (fieldSchemaPb.getTimestampPrecision() != null) {
+ fieldBuilder.setTimestampPrecision(fieldSchemaPb.getTimestampPrecision());
+ }
if (fieldSchemaPb.getDefaultValueExpression() != null) {
fieldBuilder.setDefaultValueExpression(fieldSchemaPb.getDefaultValueExpression());
}
diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java
index 0487c3f7c..cb4e44861 100644
--- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java
+++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/QueryParameterValue.java
@@ -26,6 +26,7 @@
import com.google.api.services.bigquery.model.RangeValue;
import com.google.auto.value.AutoValue;
import com.google.cloud.Timestamp;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -44,6 +45,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.threeten.extra.PeriodDuration;
@@ -76,7 +79,7 @@
@AutoValue
public abstract class QueryParameterValue implements Serializable {
- private static final DateTimeFormatter timestampFormatter =
+ static final DateTimeFormatter TIMESTAMP_FORMATTER =
new DateTimeFormatterBuilder()
.parseLenient()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
@@ -94,15 +97,21 @@ public abstract class QueryParameterValue implements Serializable {
.optionalEnd()
.toFormatter()
.withZone(ZoneOffset.UTC);
- private static final DateTimeFormatter timestampValidator =
+ private static final DateTimeFormatter TIMESTAMP_VALIDATOR =
new DateTimeFormatterBuilder()
.parseLenient()
- .append(timestampFormatter)
+ .append(TIMESTAMP_FORMATTER)
.optionalStart()
.appendOffsetId()
.optionalEnd()
.toFormatter()
.withZone(ZoneOffset.UTC);
+ // Regex to identify >9 digits in the fraction part (e.g. `.123456789123`)
+ // Matches the dot, followed by 10+ digits (fractional part), followed by non-digits (like `+00`)
+ // or end of string
+ private static final Pattern ISO8601_TIMESTAMP_HIGH_PRECISION_PATTERN =
+ Pattern.compile("\\.(\\d{10,})(?:\\D|$)");
+
private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter timeFormatter =
DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSS");
@@ -303,6 +312,9 @@ public static QueryParameterValue bytes(byte[] value) {
/**
* Creates a {@code QueryParameterValue} object with a type of TIMESTAMP.
*
+ * This method only supports microsecond precision for timestamp. To use higher precision,
+ * prefer {@link #timestamp(String)} with an ISO8601 String
+ *
* @param value Microseconds since epoch, e.g. 1733945416000000 corresponds to 2024-12-11
* 19:30:16.929Z
*/
@@ -311,8 +323,14 @@ public static QueryParameterValue timestamp(Long value) {
}
/**
- * Creates a {@code QueryParameterValue} object with a type of TIMESTAMP. Must be in the format
- * "yyyy-MM-dd HH:mm:ss.SSSSSSZZ", e.g. "2014-08-19 12:41:35.220000+00:00".
+ * Creates a {@code QueryParameterValue} object with a type of TIMESTAMP.
+ *
+ * This method supports up to picosecond precision (12 digits) for timestamp. Input should
+ * conform to ISO8601 format.
+ *
+ * Must be in the format "yyyy-MM-dd HH:mm:ss.SSSSSS{SSSSSSS}ZZ", e.g. "2014-08-19
+ * 12:41:35.123456+00:00" for microsecond precision and "2014-08-19 12:41:35.123456789123+00:00"
+ * for picosecond precision
*/
public static QueryParameterValue timestamp(String value) {
return of(value, StandardSQLTypeName.TIMESTAMP);
@@ -481,12 +499,15 @@ private static