Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.apache.arrow.vector.util.ByteArrayReadableSeekableByteChannel;
import org.apache.arrow.vector.util.JsonStringArrayList;
import org.apache.arrow.vector.util.JsonStringHashMap;
import org.apache.arrow.vector.util.Text;

/** {@link ResultSet} Implementation for Arrow datasource (Using Storage Read APIs) */
class BigQueryArrowResultSet extends BigQueryBaseResultSet {
Expand Down Expand Up @@ -345,6 +346,27 @@ private Object getObjectInternal(int columnIndex) throws SQLException {
return value;
}

@Override
public String getString(int columnIndex) throws SQLException {
checkClosed();
StandardSQLTypeName type = getStandardSQLTypeName(columnIndex);
if (type != StandardSQLTypeName.TIMESTAMP) {
return super.getString(columnIndex);
}
Object value = getObjectInternal(columnIndex);
if (value == null) {
return null;
}
if (value instanceof Text || value instanceof String) {
return BigQueryTemporalUtility.formatTimestampStringFromIso(
value.toString(), this.statement.isEnableTimestampPicos());
}
if (value instanceof Long) {
return BigQueryTemporalUtility.formatTimestampStringFromMicroseconds((Long) value);
}
Comment thread
keshavdandeva marked this conversation as resolved.
return super.getString(columnIndex);
}

@Override
public Object getObject(int columnIndex) throws SQLException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ protected SQLException createCoercionException(
cause));
}

private StandardSQLTypeName getStandardSQLTypeName(int columnIndex) throws SQLException {
protected StandardSQLTypeName getStandardSQLTypeName(int columnIndex) throws SQLException {
checkClosed();
if (isNested) {
if (columnIndex == 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection {
int highThroughputMinTableSize;
int highThroughputActivationRatio;
boolean enableSession;
boolean enableTimestampPicos;
boolean enableProjectDiscovery;
private List<String> discoveredProjectsCache;
boolean unsupportedHTAPIFallback;
Expand Down Expand Up @@ -372,6 +373,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection {
this.sslTrustStoreProvider,
this.connectionClassName);
this.enableSession = ds.getEnableSession();
this.enableTimestampPicos = ds.getEnableTimestampPicos();
this.unsupportedHTAPIFallback = ds.getUnsupportedHTAPIFallback();
this.maxResults = ds.getMaxResults();
Map<String, String> queryPropertiesMap = ds.getQueryProperties();
Expand Down Expand Up @@ -707,6 +709,10 @@ boolean isSessionEnabled() {
return this.enableSession;
}

boolean isEnableTimestampPicos() {
return this.enableTimestampPicos;
}

boolean isUnsupportedHTAPIFallback() {
return this.unsupportedHTAPIFallback;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ protected boolean removeEldestEntry(Map.Entry<String, Map<String, String>> eldes
static final String SSL_TRUST_STORE_TYPE_PROPERTY_NAME = "SSLTrustStoreType";
static final String SSL_TRUST_STORE_PROVIDER_PROPERTY_NAME = "SSLTrustStoreProvider";
static final int DEFAULT_REQUEST_GOOGLE_DRIVE_SCOPE_VALUE = 0;
static final String ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME = "EnableTimestampPicos";
static final boolean DEFAULT_ENABLE_TIMESTAMP_PICOS_VALUE = false;
static final String MAX_BYTES_BILLED_PROPERTY_NAME = "MaximumBytesBilled";
static final Long DEFAULT_MAX_BYTES_BILLED_VALUE = 0L;
static final String LABELS_PROPERTY_NAME = "Labels";
Expand Down Expand Up @@ -310,6 +312,12 @@ protected boolean removeEldestEntry(Map.Entry<String, Map<String, String>> eldes
Collections.unmodifiableSet(
new HashSet<>(
Arrays.asList(
BigQueryConnectionProperty.newBuilder()
.setName(ENABLE_TIMESTAMP_PICOS_PROPERTY_NAME)
.setDescription(
"Enable 12-digit picosecond precision for TIMESTAMP columns. Set to 1 to enable. Disabled (0) by default.")
.setDefaultValue(String.valueOf(DEFAULT_ENABLE_TIMESTAMP_PICOS_VALUE))
.build(),
BigQueryConnectionProperty.newBuilder()
.setName(MAX_BYTES_BILLED_PROPERTY_NAME)
.setDescription(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.cloud.bigquery.FieldValue.Attribute;
import com.google.cloud.bigquery.Job;
import com.google.cloud.bigquery.Schema;
import com.google.cloud.bigquery.StandardSQLTypeName;
import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException;
import java.sql.ResultSet;
import java.sql.SQLException;
Expand Down Expand Up @@ -214,6 +215,24 @@ public boolean next() throws SQLException {
}
}

@Override
public String getString(int columnIndex) throws SQLException {
checkClosed();
StandardSQLTypeName type = getStandardSQLTypeName(columnIndex);
if (type != StandardSQLTypeName.TIMESTAMP) {
return super.getString(columnIndex);
}
FieldValue value = getObjectInternal(columnIndex);
if (value == null || value.isNull()) {
return null;
}
if (value.getAttribute() == Attribute.REPEATED || value.getAttribute() == Attribute.RECORD) {
return super.getString(columnIndex);
}
return BigQueryTemporalUtility.formatTimestampString(
value.getStringValue(), this.statement.isEnableTimestampPicos());
}

@Override
public Object getObject(int columnIndex) throws SQLException {
// columnIndex is SQL index starting at 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ Statement getStatement() {
return this.statement;
}

private boolean isTimestampPicosEnabled() {
return this.statement instanceof BigQueryStatement
&& ((BigQueryStatement) this.statement).isEnableTimestampPicos();
}

private boolean supportsPicoseconds(int sqlColumn) {
Long timestampPrecision = getField(sqlColumn).getTimestampPrecision();
return timestampPrecision != null && timestampPrecision > 6;
}

private Field getField(int sqlColumn) {
return this.schemaFieldList.get(sqlColumn - 1);
}
Expand Down Expand Up @@ -116,7 +126,10 @@ public int getColumnDisplaySize(int column) {
case Types.NUMERIC:
return 14;
case Types.TIMESTAMP:
return 16;
if (isTimestampPicosEnabled() && supportsPicoseconds(column)) {
return 32;
}
return 26;
default:
return DEFAULT_DISPLAY_SIZE;
}
Expand All @@ -139,6 +152,11 @@ public int getPrecision(int column) {
return precision.intValue();
}
StandardSQLTypeName type = getStandardSQLTypeName(column);
if (type == StandardSQLTypeName.TIMESTAMP
&& isTimestampPicosEnabled()
&& supportsPicoseconds(column)) {
return 32;
}
BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo =
BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(type);
if (typeInfo != null && typeInfo.columnSize != null) {
Expand All @@ -154,6 +172,11 @@ public int getScale(int column) {
return scale.intValue();
}
StandardSQLTypeName type = getStandardSQLTypeName(column);
if (type == StandardSQLTypeName.TIMESTAMP
&& isTimestampPicosEnabled()
&& supportsPicoseconds(column)) {
return 12;
}
BigQueryJdbcTypeMappings.ColumnTypeInfo typeInfo =
BigQueryJdbcTypeMappings.STANDARD_TYPE_INFO.get(type);
if (typeInfo != null && typeInfo.decimalDigits != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@
import com.google.cloud.bigquery.exception.BigQueryJdbcSqlSyntaxErrorException;
import com.google.cloud.bigquery.storage.v1.ArrowRecordBatch;
import com.google.cloud.bigquery.storage.v1.ArrowSchema;
import com.google.cloud.bigquery.storage.v1.ArrowSerializationOptions;
import com.google.cloud.bigquery.storage.v1.BigQueryReadClient;
import com.google.cloud.bigquery.storage.v1.CreateReadSessionRequest;
import com.google.cloud.bigquery.storage.v1.DataFormat;
import com.google.cloud.bigquery.storage.v1.ReadRowsRequest;
import com.google.cloud.bigquery.storage.v1.ReadRowsResponse;
import com.google.cloud.bigquery.storage.v1.ReadSession;
import com.google.cloud.bigquery.storage.v1.ReadSession.TableReadOptions;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Uninterruptibles;
Expand Down Expand Up @@ -846,6 +848,15 @@ ResultSet processArrowResultSet(TableResult results, Job job) throws SQLExceptio
ReadSession.Builder sessionBuilder =
ReadSession.newBuilder().setTable(srcTable).setDataFormat(DataFormat.ARROW);

if (this.connection.isEnableTimestampPicos()) {
TableReadOptions.Builder tableReadOptionsBuilder = TableReadOptions.newBuilder();
tableReadOptionsBuilder
.getArrowSerializationOptionsBuilder()
.setPicosTimestampPrecision(
ArrowSerializationOptions.PicosTimestampPrecision.TIMESTAMP_PRECISION_PICOS);
sessionBuilder.setReadOptions(tableReadOptionsBuilder.build());
}

CreateReadSessionRequest.Builder builder =
CreateReadSessionRequest.newBuilder()
.setParent(parent)
Expand Down Expand Up @@ -1671,6 +1682,10 @@ public Connection getConnection() {
return this.connection;
}

boolean isEnableTimestampPicos() {
return this.connection.isEnableTimestampPicos();
}

public boolean hasMoreResults() {
if (this.parentJobId == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package com.google.cloud.bigquery.jdbc;

import com.google.common.base.Strings;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
Expand All @@ -24,6 +27,8 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;

/**
Expand All @@ -32,6 +37,9 @@
*/
final class BigQueryTemporalUtility {

private static final DateTimeFormatter UTC_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneOffset.UTC);

private BigQueryTemporalUtility() {}

/**
Expand Down Expand Up @@ -89,6 +97,28 @@ public static Time boxTime(String val, ZoneId zoneId) {
return new Time(targetCal.getTimeInMillis());
}

/**
* Truncates a BigQuery timestamp string to 9 fractional digits (nanoseconds) because
* Instant.parse throws DateTimeParseException for >9 digits, and java.sql.Timestamp maxes out at
* nanos anyway.
*/
private static String truncateToNanoseconds(String iso) {
int dotIdx = iso.indexOf('.');
// Fast path: if there is no dot or at most 9 fractional digits after the dot, return as-is.
if (dotIdx == -1 || iso.length() - dotIdx <= 10) {
return iso;
}

int fractionEnd = dotIdx + 1;
while (fractionEnd < iso.length() && Character.isDigit(iso.charAt(fractionEnd))) {
fractionEnd++;
}
if (fractionEnd - dotIdx - 1 > 9) {
return iso.substring(0, dotIdx + 10) + iso.substring(fractionEnd);
}
return iso;
}

/**
* Converts a BigQuery absolute TIMESTAMP string into a legacy Timestamp. Because it is absolute,
* the Calendar timezone is explicitly ignored per JDBC 4.2 spec.
Expand All @@ -105,11 +135,100 @@ public static Timestamp boxTimestamp(String val) {
iso = iso.substring(0, 10) + 'T' + iso.substring(11);
}

iso = truncateToNanoseconds(iso);

try {
return Timestamp.from(Instant.parse(iso));
} catch (java.time.format.DateTimeParseException e) {
// Fallback for non-standard formats
return Timestamp.valueOf(val);
return Timestamp.valueOf(truncateToNanoseconds(val));
}
}

/**
* Parses a numeric epoch decimal string (e.g. from BigQuery REST JSON) into a JSR-310 {@link
* Instant}. Sub-nanosecond precision is deterministically truncated (floor/down) rather than
* rounded to avoid boundary rollovers.
*/
public static Instant parseEpochDecimalToInstant(String epochDecimal) {
if (epochDecimal == null) {
return null;
}
BigDecimal bd = new BigDecimal(epochDecimal);
long seconds = bd.setScale(0, RoundingMode.FLOOR).longValue();
long nanos =
bd.subtract(BigDecimal.valueOf(seconds))
.movePointRight(9)
.setScale(0, RoundingMode.DOWN)
.longValue();
return Instant.ofEpochSecond(seconds, nanos);
}

/**
* Formats a numeric epoch decimal string into standard SQL timestamp string format ("yyyy-MM-dd
* HH:mm:ss.ffffff[ffffff]"). Sub-microsecond / sub-picosecond precision is deterministically
* truncated (down) to prevent timestamp boundary rollovers.
*/
public static String formatTimestampString(String epochDecimal, boolean enableTimestampPicos) {
if (epochDecimal == null) {
return null;
}

BigDecimal bd = new BigDecimal(epochDecimal);
long seconds = bd.setScale(0, RoundingMode.FLOOR).longValue();
BigDecimal fractionalSeconds = bd.subtract(BigDecimal.valueOf(seconds));

int originalScale = bd.scale() > 0 ? bd.scale() : 0;
int scale = enableTimestampPicos ? Math.max(6, Math.min(12, originalScale)) : 6;

String fraction =
fractionalSeconds.setScale(scale, RoundingMode.DOWN).toPlainString().substring(2);

Instant instant = Instant.ofEpochSecond(seconds);
return UTC_FORMATTER.format(instant) + "." + fraction;
}

public static String formatTimestampStringFromMicroseconds(long microseconds) {
long seconds = Math.floorDiv(microseconds, 1000000L);
long micros = Math.floorMod(microseconds, 1000000L);

String fraction = Strings.padStart(Long.toString(micros), 6, '0');
Instant instant = Instant.ofEpochSecond(seconds);
return UTC_FORMATTER.format(instant) + "." + fraction;
}

public static String formatTimestampStringFromIso(
String isoString, boolean enableTimestampPicos) {
if (isoString == null) {
return null;
}

String s = isoString;
if (s.endsWith(" UTC")) {
s = s.substring(0, s.length() - 4);
} else if (s.endsWith("Z")) {
s = s.substring(0, s.length() - 1);
}

if (s.length() > 10 && s.charAt(10) == 'T') {
s = s.substring(0, 10) + ' ' + s.substring(11);
}

int dotIdx = s.indexOf('.');
if (dotIdx == -1) {
return s + ".000000";
}

String base = s.substring(0, dotIdx);
String fraction = s.substring(dotIdx + 1);

int maxScale = enableTimestampPicos ? 12 : 6;
if (fraction.length() > maxScale) {
fraction = fraction.substring(0, maxScale);
} else if (fraction.length() < 6) {
fraction = Strings.padEnd(fraction, 6, '0');
}

return base + "." + fraction;
}
}
Loading
Loading