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 @@ -52,8 +52,13 @@
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.List;
import javax.sql.rowset.serial.SerialBlob;
Expand Down Expand Up @@ -317,9 +322,12 @@ protected Object getObject(int columnIndex, ZoneId zone) throws SQLException {
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
LOG.finestTrace("getObject");
try {
if (type == LocalTime.class || type == String.class) {
if (isRawViewTarget(type)) {
StandardSQLTypeName bqType = getStandardSQLTypeName(columnIndex);
if (bqType == StandardSQLTypeName.TIME) {
// java.sql types can't hold every value of these (sub-ms TIME, pre-1582 DATE/DATETIME).
if (bqType == StandardSQLTypeName.TIME
|| bqType == StandardSQLTypeName.DATE
|| bqType == StandardSQLTypeName.DATETIME) {
return BigQueryTypeRegistry.convert(getObjectInternal(columnIndex), bqType, type, null);
}
}
Expand All @@ -339,6 +347,16 @@ public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
}
}

private static boolean isRawViewTarget(Class<?> type) {
return type == String.class
|| type == LocalDate.class
|| type == LocalDateTime.class
|| type == LocalTime.class
|| type == Instant.class
|| type == OffsetDateTime.class
|| type == ZonedDateTime.class;
}

@Override
public <T> T getObject(String columnLabel, Class<T> type) throws SQLException {
return getObject(getColumnIndex(columnLabel), type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
Expand Down Expand Up @@ -77,11 +78,43 @@ public static Timestamp boxDateTime(String val, ZoneId zoneId) {

/**
* Converts a civil {@link LocalDateTime} into a {@link Timestamp} by anchoring it to the provided
* timezone (or JVM default if null).
* timezone (or JVM default if null). Uses java.sql's calendar so the Timestamp keeps the digits
* before 1582 and before standard time, where java.time disagrees.
*/
static Timestamp boxDateTime(LocalDateTime localDateTime, ZoneId zoneId) {
ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault();
return Timestamp.from(localDateTime.atZone(targetZone).toInstant());
if (zoneId == null) {
return Timestamp.valueOf(localDateTime);
}
GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone(zoneId));
cal.clear();
cal.set(
localDateTime.getYear(),
localDateTime.getMonthValue() - 1,
localDateTime.getDayOfMonth(),
localDateTime.getHour(),
localDateTime.getMinute(),
localDateTime.getSecond());
Comment thread
keshavdandeva marked this conversation as resolved.
Timestamp timestamp = new Timestamp(cal.getTimeInMillis());
timestamp.setNanos(localDateTime.getNano());
return timestamp;
}

/** Inverse of {@link #boxDateTime(LocalDateTime, ZoneId)}. */
static LocalDateTime unboxDateTime(Timestamp timestamp, ZoneId zoneId) {
if (zoneId == null) {
return timestamp.toLocalDateTime();
}
GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone(zoneId));
cal.setTimeInMillis(timestamp.getTime());
int year = cal.get(Calendar.YEAR);
return LocalDateTime.of(
cal.get(Calendar.ERA) == GregorianCalendar.BC ? 1 - year : year,
cal.get(Calendar.MONTH) + 1,
cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY),
cal.get(Calendar.MINUTE),
cal.get(Calendar.SECOND),
timestamp.getNanos());
}

/**
Expand All @@ -97,8 +130,10 @@ public static Date boxDate(String val, ZoneId zoneId) {
* provided timezone (or JVM default if null).
*/
static Date boxDate(LocalDate localDate, ZoneId zoneId) {
ZoneId targetZone = zoneId != null ? zoneId : ZoneId.systemDefault();
return new Date(localDate.atStartOfDay(targetZone).toInstant().toEpochMilli());
if (zoneId == null) {
return Date.valueOf(localDate);
}
return new Date(boxDateTime(localDate.atStartOfDay(), zoneId).getTime());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,35 +227,49 @@ static TypeDescriptor<?> createDateDescriptor() {
Arrays.asList(Date.class, LocalDate.class),
(val, targetClass, zone) -> {
// Modern fast-path: Bypass intermediate object creation for JSR-310 targets
if (targetClass == LocalDate.class) {
if (targetClass == LocalDate.class
|| targetClass == String.class
|| targetClass == LocalDateTime.class
|| isInstantView(targetClass)) {
LocalDate localDate;
if (val instanceof LocalDate) {
return val;
localDate = (LocalDate) val;
} else if (val instanceof Date) {
return ((Date) val).toLocalDate();
localDate = ((Date) val).toLocalDate();
} else if (val instanceof Timestamp) {
return ((Timestamp) val).toLocalDateTime().toLocalDate();
localDate = ((Timestamp) val).toLocalDateTime().toLocalDate();
Comment thread
keshavdandeva marked this conversation as resolved.
} else if (val instanceof java.sql.Time) {
throw new BigQueryJdbcException("Cannot convert to DATE: " + val);
} else if (val instanceof java.util.Date) {
return new Date(((java.util.Date) val).getTime()).toLocalDate();
localDate = new Date(((java.util.Date) val).getTime()).toLocalDate();
} else if (val instanceof LocalDateTime) {
return ((LocalDateTime) val).toLocalDate();
localDate = ((LocalDateTime) val).toLocalDate();
} else if (val instanceof Integer) {
return LocalDate.ofEpochDay(((Integer) val).longValue());
localDate = LocalDate.ofEpochDay(((Integer) val).longValue());
} else if (val instanceof String) {
return LocalDate.parse((String) val);
localDate = LocalDate.parse((String) val);
} else {
throw new BigQueryJdbcException("Cannot convert to DATE: " + val);
}
if (targetClass == LocalDate.class) {
return localDate;
}
if (targetClass == String.class) {
return localDate.toString();
}
if (targetClass == LocalDateTime.class) {
return localDate.atStartOfDay();
}
return anchorCivil(localDate.atStartOfDay(), targetClass, zone);
}

// Legacy path: Box values into java.sql.Date
Date sqlDate;
if (val instanceof Date) {
sqlDate = (Date) val;
} else if (val instanceof Timestamp) {
ZoneId targetZone = zone != null ? zone : ZoneId.systemDefault();
LocalDate localDate = ((Timestamp) val).toInstant().atZone(targetZone).toLocalDate();
LocalDate localDate =
BigQueryTemporalUtility.unboxDateTime((Timestamp) val, zone).toLocalDate();
sqlDate = BigQueryTemporalUtility.boxDate(localDate, zone);
} else if (val instanceof java.sql.Time) {
throw new BigQueryJdbcException("Cannot convert to DATE: " + val);
Expand Down Expand Up @@ -286,21 +300,39 @@ static TypeDescriptor<?> createDatetimeDescriptor() {
Arrays.asList(Timestamp.class, LocalDateTime.class),
(val, targetClass, zone) -> {
// Modern fast-path: Bypass intermediate object creation for JSR-310 targets
if (targetClass == LocalDateTime.class) {
if (targetClass == LocalDateTime.class
|| targetClass == String.class
|| targetClass == LocalDate.class
|| targetClass == LocalTime.class
|| isInstantView(targetClass)) {
LocalDateTime localDateTime;
if (val instanceof LocalDateTime) {
return val;
localDateTime = (LocalDateTime) val;
} else if (val instanceof Timestamp) {
return ((Timestamp) val).toLocalDateTime();
localDateTime = ((Timestamp) val).toLocalDateTime();
Comment thread
keshavdandeva marked this conversation as resolved.
} else if (val instanceof java.util.Date) {
return new Timestamp(((java.util.Date) val).getTime()).toLocalDateTime();
localDateTime = new Timestamp(((java.util.Date) val).getTime()).toLocalDateTime();
} else if (val instanceof String) {
String isoString =
BigQueryTemporalUtility.truncateFractionalSeconds(
((String) val).replace(' ', 'T'), 9);
return LocalDateTime.parse(isoString);
localDateTime = LocalDateTime.parse(isoString);
} else {
throw new BigQueryJdbcException("Cannot convert to DATETIME: " + val);
}
if (targetClass == LocalDateTime.class) {
return localDateTime;
}
if (targetClass == String.class) {
return TIMESTAMP_FORMATTER.format(localDateTime);
}
if (targetClass == LocalDate.class) {
return localDateTime.toLocalDate();
}
if (targetClass == LocalTime.class) {
return localDateTime.toLocalTime();
}
return anchorCivil(localDateTime, targetClass, zone);
}

// Legacy path: Box values into java.sql.Timestamp
Expand Down Expand Up @@ -329,10 +361,13 @@ static TypeDescriptor<?> createTimestampDescriptor() {
Arrays.asList(Timestamp.class, OffsetDateTime.class, Instant.class, ZonedDateTime.class),
(val, targetClass, zone) -> {
// Modern fast-path: Bypass intermediate object creation for JSR-310 targets
if (targetClass == Instant.class
|| targetClass == OffsetDateTime.class
|| targetClass == ZonedDateTime.class
|| targetClass == String.class) {
if (isInstantView(targetClass)
|| targetClass == String.class
|| targetClass == LocalDateTime.class
|| targetClass == LocalDate.class
|| targetClass == LocalTime.class
|| targetClass == Date.class
|| targetClass == Time.class) {
Instant instant;
if (val instanceof Instant) {
instant = (Instant) val;
Expand All @@ -354,18 +389,27 @@ static TypeDescriptor<?> createTimestampDescriptor() {
throw new BigQueryJdbcException("Cannot convert to TIMESTAMP: " + val);
}

if (targetClass == Instant.class) {
return instant;
}
if (targetClass == OffsetDateTime.class) {
return instant.atOffset(ZoneOffset.UTC);
}
if (targetClass == ZonedDateTime.class) {
return instant.atZone(ZoneOffset.UTC);
if (isInstantView(targetClass)) {
return renderInstant(instant, targetClass);
}
if (targetClass == String.class) {
return TIMESTAMP_FORMATTER.format(instant.atOffset(ZoneOffset.UTC));
}
// java.time, not Timestamp.toLocalDateTime(), which shifts dates before 1582.
ZonedDateTime local = instant.atZone(zone != null ? zone : ZoneId.systemDefault());
if (targetClass == LocalDateTime.class) {
return local.toLocalDateTime();
}
if (targetClass == LocalDate.class) {
return local.toLocalDate();
}
if (targetClass == LocalTime.class) {
return local.toLocalTime();
}
if (targetClass == Date.class) {
return BigQueryTemporalUtility.boxDate(local.toLocalDate(), zone);
}
return BigQueryTemporalUtility.boxTime(local.toLocalTime(), zone);
}

// Legacy path: Box values into java.sql.Timestamp
Expand Down Expand Up @@ -434,8 +478,8 @@ static TypeDescriptor<?> createTimeDescriptor() {
Time sqlTime;
if (val instanceof Time) sqlTime = (Time) val;
else if (val instanceof Timestamp) {
ZoneId targetZone = zone != null ? zone : ZoneId.systemDefault();
LocalTime localTime = ((Timestamp) val).toInstant().atZone(targetZone).toLocalTime();
LocalTime localTime =
BigQueryTemporalUtility.unboxDateTime((Timestamp) val, zone).toLocalTime();
sqlTime = BigQueryTemporalUtility.boxTime(localTime, zone);
} else if (val instanceof Date) {
throw new BigQueryJdbcException("Cannot convert to TIME: " + val);
Expand Down Expand Up @@ -639,6 +683,27 @@ private static void register(TypeDescriptor<?> descriptor) {
}
}

private static boolean isInstantView(Class<?> targetClass) {
return targetClass == Instant.class
|| targetClass == OffsetDateTime.class
|| targetClass == ZonedDateTime.class;
}

private static Object renderInstant(Instant instant, Class<?> targetClass) {
if (targetClass == OffsetDateTime.class) {
return instant.atOffset(ZoneOffset.UTC);
}
if (targetClass == ZonedDateTime.class) {
return instant.atZone(ZoneOffset.UTC);
}
return instant;
}

private static Object anchorCivil(LocalDateTime civil, Class<?> targetClass, ZoneId zone) {
ZoneId targetZone = zone != null ? zone : ZoneId.systemDefault();
return renderInstant(civil.atZone(targetZone).toInstant(), targetClass);
}

private BigQueryTypeRegistry() {}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.Calendar;
import java.util.TimeZone;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class BigQueryTemporalUtilityTest {

Expand Down Expand Up @@ -225,6 +227,37 @@ public void testBoxDateTimeAndDate() {
assertThat(picosDt.getNanos()).isEqualTo(123456789);
}

/** Pre-1582 and pre-standard-time digits, with and without a Calendar zone. */
@ParameterizedTest
@ValueSource(strings = {"UTC", "America/Los_Angeles", "Asia/Kolkata"})
public void testBoxCivilValuesKeepDigits(String jvmZone) {
TimeZone originalDefault = TimeZone.getDefault();
try {
TimeZone.setDefault(TimeZone.getTimeZone(jvmZone));
ZoneId calZone = ZoneId.of("Asia/Tokyo");
for (String digits : new String[] {"0001-01-01", "1582-10-04", "1850-06-01"}) {
LocalDateTime noon = LocalDateTime.parse(digits + "T12:00:00");
assertThat(BigQueryTemporalUtility.boxDate(digits, null).toLocalDate())
.isEqualTo(LocalDate.parse(digits));
assertThat(
BigQueryTemporalUtility.boxDateTime(digits + " 12:00:00", null).toLocalDateTime())
.isEqualTo(noon);
assertThat(
BigQueryTemporalUtility.unboxDateTime(
BigQueryTemporalUtility.boxDateTime(noon, calZone), calZone))
.isEqualTo(noon);
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(calZone));
cal.setTime(BigQueryTemporalUtility.boxDate(digits, calZone));
assertThat(
LocalDate.of(
cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DATE)))
.isEqualTo(LocalDate.parse(digits));
}
} finally {
TimeZone.setDefault(originalDefault);
}
}

@Test
public void testBoxTime() {
// Null zone (modern JSR-310)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,19 @@ public void testTimestampReadCoercionsAreJvmZoneInvariant(String zone) throws Ex
BigQueryTypeRegistry.convert(
epochMicros, StandardSQLTypeName.TIMESTAMP, Instant.class, null))
.isEqualTo(microsInstant);

// Local views before 1582 follow java.time.
Instant yearOne = Instant.parse("0001-01-01T00:00:00Z");
long yearOneMicros = yearOne.getEpochSecond() * 1_000_000L;
ZonedDateTime yearOneLocal = yearOne.atZone(ZoneId.of(zone));
assertThat(
BigQueryTypeRegistry.convert(
yearOneMicros, StandardSQLTypeName.TIMESTAMP, LocalDateTime.class, null))
.isEqualTo(yearOneLocal.toLocalDateTime());
assertThat(
BigQueryTypeRegistry.convert(
yearOneMicros, StandardSQLTypeName.TIMESTAMP, LocalDate.class, null))
.isEqualTo(yearOneLocal.toLocalDate());
}

@Test
Expand Down
Loading
Loading