Skip to content
Open
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 @@ -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 @@ -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,23 @@ 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());
Comment thread
keshavdandeva marked this conversation as resolved.
}

@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 @@ -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,109 @@ 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));
}
Comment thread
keshavdandeva marked this conversation as resolved.
}

/**
* 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"). Sub-microsecond precision is deterministically truncated (down) to prevent
* timestamp boundary rollovers.
*/
public static String formatTimestampString(String epochDecimal) {
return formatTimestampString(epochDecimal, false);
}

/**
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ static Timestamp convertTimestampWithCalendar(Timestamp timestamp, Calendar cal)
LocalDateTime.class,
Timestamp.class)
.registerTypeCoercion(Text::toString, Text.class, String.class)
.registerTypeCoercion(
text -> BigQueryTemporalUtility.boxTimestamp(text.toString()),
Text.class,
Timestamp.class)
.registerTypeCoercion(
BigQueryTemporalUtility::boxTimestamp, String.class, Timestamp.class)
.registerTypeCoercion(new TextToInteger())
.registerTypeCoercion(new LongToTimestamp())
.registerTypeCoercion(new LongToTime())
Expand Down Expand Up @@ -438,9 +444,9 @@ public Timestamp coerce(FieldValue fieldValue) {
// Timestamp.valueOf() expects "yyyy-mm-dd hh:mm:ss.fffffffff" format.
return Timestamp.valueOf(rawValue.replace('T', ' '));
} else {
Comment thread
keshavdandeva marked this conversation as resolved.
// It's a TIMESTAMP numeric string.
long microseconds = fieldValue.getTimestampValue();
Instant instant = Instant.EPOCH.plus(microseconds, ChronoUnit.MICROS);
// Numeric epoch decimal string from BigQuery JSON (e.g. "1775642400.123456789123" or
// "1.6905474E9")
Instant instant = BigQueryTemporalUtility.parseEpochDecimalToInstant(rawValue);
// Timezone-agnostic conversion preserving exact point in time as mandated by JDBC spec
return Timestamp.from(instant);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ public static Collection<Object[]> data() {
TIMESTAMP,
arraySchemaAndValue(
TIMESTAMP,
"1680174859.8202269",
"1680261259.8202269",
"1680347659.8202269",
"1680434059.8202269"),
"1680174859.820227",
"1680261259.820227",
"1680347659.820227",
"1680434059.820227"),
new Timestamp[] {
Timestamp.valueOf(aTimeStamp), // 2023-03-30 16:44:19.82
Timestamp.valueOf(aTimeStamp.plusDays(1)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ public void testIteration() throws SQLException {
assertThat(bigQueryJsonResultSet.getFloat(3)).isEqualTo(1.5f);
assertThat(bigQueryJsonResultSet.getString("fourth")).isEqualTo(STRING_VAL);
assertThat(bigQueryJsonResultSet.getString(4)).isEqualTo(STRING_VAL);
assertThat(bigQueryJsonResultSet.getString("fifth")).isEqualTo("2023-03-30 11:14:19.820000");
assertThat(bigQueryJsonResultSet.getString(5)).isEqualTo("2023-03-30 11:14:19.820000");
assertThat(bigQueryJsonResultSet.getTimestamp("fifth"))
.isEqualTo(Timestamp.valueOf(aTimeStamp));
assertThat(bigQueryJsonResultSet.getTimestamp(5)).isEqualTo(Timestamp.valueOf(aTimeStamp));
Expand Down Expand Up @@ -506,6 +508,24 @@ public void testGetObjectWithType_failure(Object column, Class<?> type) throws S
}
}

@Test
public void testGetString_timestamp() throws SQLException {
assertThat(resetResultSet()).isTrue();
bigQueryJsonResultSet.next();
assertThat(bigQueryJsonResultSet.getString("fifth")).isEqualTo("2023-03-30 11:14:19.820000");
assertThat(bigQueryJsonResultSet.getString(5)).isEqualTo("2023-03-30 11:14:19.820000");
}

@Test
public void testGetString_structAndArray() throws SQLException {
assertThat(resetResultSet()).isTrue();
bigQueryJsonResultSet.next();
assertThat(bigQueryJsonResultSet.getString("eight")).isEqualTo("[10, 20]");
assertThat(bigQueryJsonResultSet.getString(8)).isEqualTo("[10, 20]");
assertThat(bigQueryJsonResultSet.getString("ninth")).isNotNull();
assertThat(bigQueryJsonResultSet.getString(9)).isNotNull();
}

private int resultSetRowCount(BigQueryJsonResultSet resultSet) throws SQLException {
int rowCount = 0;
while (resultSet.next()) {
Expand Down
Loading
Loading