Skip to content

Commit

Permalink
Represent nanosecond-precision timestamps with BigDecimal
Browse files Browse the repository at this point in the history
  • Loading branch information
wnob committed Jan 14, 2023
1 parent dcc0ff3 commit 18f3f91
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
Expand Down Expand Up @@ -161,26 +162,33 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData,
throw new AssertionError("bad " + columnMetaData.type.rep);
}
case Types.TIMESTAMP:
// TIMESTAMP WITH LOCAL TIME ZONE is a standard ISO type without proper JDBC support.
// It represents a global instant in time, as opposed to local clock/calendar parameters,
// so avoid normalizing against the local calendar by setting that to null for this type.
Calendar effectiveCalendar =
"TIMESTAMP_WITH_LOCAL_TIME_ZONE".equals(columnMetaData.type.getName())
? null
: localCalendar;
switch (columnMetaData.type.rep) {
case PRIMITIVE_LONG:
case LONG:
case NUMBER:
return new TimestampFromNumberAccessor(getter, localCalendar);
return new TimestampFromNumberAccessor(getter, effectiveCalendar);
case JAVA_SQL_TIMESTAMP:
return new TimestampAccessor(getter);
case JAVA_UTIL_DATE:
return new TimestampFromUtilDateAccessor(getter, localCalendar);
return new TimestampFromUtilDateAccessor(getter, effectiveCalendar);
default:
throw new AssertionError("bad " + columnMetaData.type.rep);
}
case 2013: // TIME_WITH_TIMEZONE
case Types.TIME_WITH_TIMEZONE:
switch (columnMetaData.type.rep) {
case STRING:
return new StringAccessor(getter);
default:
throw new AssertionError("bad " + columnMetaData.type.rep);
}
case 2014: // TIMESTAMP_WITH_TIMEZONE
case Types.TIMESTAMP_WITH_TIMEZONE:
switch (columnMetaData.type.rep) {
case STRING:
return new StringAccessor(getter);
Expand Down Expand Up @@ -276,11 +284,31 @@ static Time intToTime(int v, Calendar calendar) {
return new Time(v);
}

static Timestamp longToTimestamp(long v, Calendar calendar) {
/**
* Interpret a {@link Number} as a {@link Timestamp}.
*
* If the number is a {@link BigDecimal}, assume it represents seconds since epoch with up to
* nanosecond precision. If it is any other {@link Number}, truncate it to an integer and assume
* it represents milliseconds since epoch.
*
* @param v The number to convert
* @param calendar Subtract the time zone offset of this calendar from the result
*/
static Timestamp numberToTimestamp(Number v, Calendar calendar) {
Instant instant;
if (v instanceof BigDecimal) {
// May overflow if the value is > ~292 *billion* years away from epoch in either direction.
long wholeSeconds = v.longValue();
long nanoSeconds = ((BigDecimal) v).remainder(BigDecimal.ONE).movePointRight(9).longValue();
instant = Instant.ofEpochSecond(wholeSeconds, nanoSeconds);
} else {
// May overflow if the value is > ~292 *million* years away from epoch in either direction.
instant = Instant.ofEpochMilli(v.longValue());
}
if (calendar != null) {
v -= calendar.getTimeZone().getOffset(v);
instant = instant.minusMillis(calendar.getTimeZone().getOffset(instant.toEpochMilli()));
}
return new Timestamp(v);
return Timestamp.from(instant);
}

/** Implementation of {@link Cursor.Accessor}. */
Expand Down Expand Up @@ -934,8 +962,7 @@ private DateFromNumberAccessor(Getter getter, Calendar localCalendar) {
if (v == null) {
return null;
}
return longToTimestamp(v.longValue() * DateTimeUtils.MILLIS_PER_DAY,
calendar);
return numberToTimestamp(v.longValue() * DateTimeUtils.MILLIS_PER_DAY, calendar);
}

@Override public String getString() throws SQLException {
Expand Down Expand Up @@ -990,7 +1017,7 @@ private TimeFromNumberAccessor(Getter getter, Calendar localCalendar) {
if (v == null) {
return null;
}
return longToTimestamp(v.longValue(), calendar);
return numberToTimestamp(v, calendar);
}

@Override public String getString() throws SQLException {
Expand Down Expand Up @@ -1018,10 +1045,10 @@ protected Number getNumber() throws SQLException {
* in its default representation {@code long};
* corresponds to {@link java.sql.Types#TIMESTAMP}.
*/
private static class TimestampFromNumberAccessor extends NumberAccessor {
static class TimestampFromNumberAccessor extends NumberAccessor {
private final Calendar localCalendar;

private TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
super(getter, 0);
this.localCalendar = localCalendar;
}
Expand All @@ -1035,7 +1062,7 @@ private TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
if (v == null) {
return null;
}
return longToTimestamp(v.longValue(), calendar);
return numberToTimestamp(v, calendar);
}

@Override public Date getDate(Calendar calendar) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.calcite.avatica.util;

import org.apache.calcite.avatica.util.AbstractCursor.Getter;
import org.apache.calcite.avatica.util.AbstractCursor.TimestampFromNumberAccessor;

import org.junit.Test;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

import static org.junit.Assert.assertEquals;

/** Unit tests for {@link TimestampFromNumberAccessor} */
public class TimestampFromNumberAccessorTest {

// An example of a calendar that observes DST.
private static final Calendar LOS_ANGELES_CALENDAR =
GregorianCalendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"), Locale.ROOT);

@Test
public void testNoOffset() throws SQLException {
test(
1673657135052L, // UTC: 2023-01-14 00:45:23.052
null,
parseUtc("2023-01-14T00:45:35.052"));
}

@Test
public void testNoOffsetNanoseconds() throws SQLException {
test(
new BigDecimal("1673657135.052637485"), // UTC: 2023-01-14 00:45:23.052637485
null,
parseUtc("2023-01-14T00:45:35.052637485"));
}

@Test
public void testNoOffsetDaylightSavings() throws SQLException {
test(
1689320723052L, // UTC: 2023-07-14 07:45:23.052
null,
parseUtc("2023-07-14T07:45:23.052"));
}

@Test
public void testNoOffsetDaylightSavingsNanoseconds() throws SQLException {
test(
new BigDecimal("1689320723.052637485"), // UTC: 2023-07-14 07:45:23.052637485
null,
parseUtc("2023-07-14T07:45:23.052637485"));
}

@Test
public void testWithOffset() throws SQLException {
test(
1673657135052L, // UTC: 2023-01-14 00:45:23.052
LOS_ANGELES_CALENDAR,
parseUtc("2023-01-14T08:45:35.052"));
}

@Test
public void testWithOffsetNanoseconds() throws SQLException {
test(
new BigDecimal("1673657135.052637485"), // UTC: 2023-01-14 00:45:23.052637485
LOS_ANGELES_CALENDAR,
parseUtc("2023-01-14T08:45:35.052637485"));
}

@Test
public void testWithOffsetDaylightSavings() throws SQLException {
test(
1689320723052L, // UTC: 2023-07-14 07:45:23.052
LOS_ANGELES_CALENDAR,
parseUtc("2023-07-14T14:45:23.052"));
}

@Test
public void testWithOffsetDaylightSavingsNanoseconds() throws SQLException {
test(
new BigDecimal("1689320723.052637485"), // UTC: 2023-07-14 07:45:23.052637485
LOS_ANGELES_CALENDAR,
parseUtc("2023-07-14T14:45:23.052637485"));
}

private static void test(Number v, Calendar calendar, Timestamp expectedValue)
throws SQLException {
TimestampFromNumberAccessor accessor =
new TimestampFromNumberAccessor(
new Getter() {
@Override
public Object getObject() {
return v;
}

@Override
public boolean wasNull() {
return v == null;
}
},
calendar);

assertEquals(expectedValue, accessor.getTimestamp(calendar));
assertEquals(expectedValue, accessor.getObject());
}

private static Timestamp parseUtc(String utcTimestamp) {
return Timestamp.from(LocalDateTime.parse(utcTimestamp).toInstant(ZoneOffset.UTC));
}
}

0 comments on commit 18f3f91

Please sign in to comment.