Skip to content

Commit 18f3f91

Browse files
committed
Represent nanosecond-precision timestamps with BigDecimal
1 parent dcc0ff3 commit 18f3f91

File tree

2 files changed

+171
-13
lines changed

2 files changed

+171
-13
lines changed

core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.sql.Time;
4040
import java.sql.Timestamp;
4141
import java.sql.Types;
42+
import java.time.Instant;
4243
import java.util.ArrayList;
4344
import java.util.Calendar;
4445
import java.util.List;
@@ -161,26 +162,33 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData,
161162
throw new AssertionError("bad " + columnMetaData.type.rep);
162163
}
163164
case Types.TIMESTAMP:
165+
// TIMESTAMP WITH LOCAL TIME ZONE is a standard ISO type without proper JDBC support.
166+
// It represents a global instant in time, as opposed to local clock/calendar parameters,
167+
// so avoid normalizing against the local calendar by setting that to null for this type.
168+
Calendar effectiveCalendar =
169+
"TIMESTAMP_WITH_LOCAL_TIME_ZONE".equals(columnMetaData.type.getName())
170+
? null
171+
: localCalendar;
164172
switch (columnMetaData.type.rep) {
165173
case PRIMITIVE_LONG:
166174
case LONG:
167175
case NUMBER:
168-
return new TimestampFromNumberAccessor(getter, localCalendar);
176+
return new TimestampFromNumberAccessor(getter, effectiveCalendar);
169177
case JAVA_SQL_TIMESTAMP:
170178
return new TimestampAccessor(getter);
171179
case JAVA_UTIL_DATE:
172-
return new TimestampFromUtilDateAccessor(getter, localCalendar);
180+
return new TimestampFromUtilDateAccessor(getter, effectiveCalendar);
173181
default:
174182
throw new AssertionError("bad " + columnMetaData.type.rep);
175183
}
176-
case 2013: // TIME_WITH_TIMEZONE
184+
case Types.TIME_WITH_TIMEZONE:
177185
switch (columnMetaData.type.rep) {
178186
case STRING:
179187
return new StringAccessor(getter);
180188
default:
181189
throw new AssertionError("bad " + columnMetaData.type.rep);
182190
}
183-
case 2014: // TIMESTAMP_WITH_TIMEZONE
191+
case Types.TIMESTAMP_WITH_TIMEZONE:
184192
switch (columnMetaData.type.rep) {
185193
case STRING:
186194
return new StringAccessor(getter);
@@ -276,11 +284,31 @@ static Time intToTime(int v, Calendar calendar) {
276284
return new Time(v);
277285
}
278286

279-
static Timestamp longToTimestamp(long v, Calendar calendar) {
287+
/**
288+
* Interpret a {@link Number} as a {@link Timestamp}.
289+
*
290+
* If the number is a {@link BigDecimal}, assume it represents seconds since epoch with up to
291+
* nanosecond precision. If it is any other {@link Number}, truncate it to an integer and assume
292+
* it represents milliseconds since epoch.
293+
*
294+
* @param v The number to convert
295+
* @param calendar Subtract the time zone offset of this calendar from the result
296+
*/
297+
static Timestamp numberToTimestamp(Number v, Calendar calendar) {
298+
Instant instant;
299+
if (v instanceof BigDecimal) {
300+
// May overflow if the value is > ~292 *billion* years away from epoch in either direction.
301+
long wholeSeconds = v.longValue();
302+
long nanoSeconds = ((BigDecimal) v).remainder(BigDecimal.ONE).movePointRight(9).longValue();
303+
instant = Instant.ofEpochSecond(wholeSeconds, nanoSeconds);
304+
} else {
305+
// May overflow if the value is > ~292 *million* years away from epoch in either direction.
306+
instant = Instant.ofEpochMilli(v.longValue());
307+
}
280308
if (calendar != null) {
281-
v -= calendar.getTimeZone().getOffset(v);
309+
instant = instant.minusMillis(calendar.getTimeZone().getOffset(instant.toEpochMilli()));
282310
}
283-
return new Timestamp(v);
311+
return Timestamp.from(instant);
284312
}
285313

286314
/** Implementation of {@link Cursor.Accessor}. */
@@ -934,8 +962,7 @@ private DateFromNumberAccessor(Getter getter, Calendar localCalendar) {
934962
if (v == null) {
935963
return null;
936964
}
937-
return longToTimestamp(v.longValue() * DateTimeUtils.MILLIS_PER_DAY,
938-
calendar);
965+
return numberToTimestamp(v.longValue() * DateTimeUtils.MILLIS_PER_DAY, calendar);
939966
}
940967

941968
@Override public String getString() throws SQLException {
@@ -990,7 +1017,7 @@ private TimeFromNumberAccessor(Getter getter, Calendar localCalendar) {
9901017
if (v == null) {
9911018
return null;
9921019
}
993-
return longToTimestamp(v.longValue(), calendar);
1020+
return numberToTimestamp(v, calendar);
9941021
}
9951022

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

1024-
private TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
1051+
TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
10251052
super(getter, 0);
10261053
this.localCalendar = localCalendar;
10271054
}
@@ -1035,7 +1062,7 @@ private TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
10351062
if (v == null) {
10361063
return null;
10371064
}
1038-
return longToTimestamp(v.longValue(), calendar);
1065+
return numberToTimestamp(v, calendar);
10391066
}
10401067

10411068
@Override public Date getDate(Calendar calendar) throws SQLException {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.calcite.avatica.util;
18+
19+
import org.apache.calcite.avatica.util.AbstractCursor.Getter;
20+
import org.apache.calcite.avatica.util.AbstractCursor.TimestampFromNumberAccessor;
21+
22+
import org.junit.Test;
23+
24+
import java.math.BigDecimal;
25+
import java.sql.SQLException;
26+
import java.sql.Timestamp;
27+
import java.time.LocalDateTime;
28+
import java.time.ZoneOffset;
29+
import java.util.Calendar;
30+
import java.util.GregorianCalendar;
31+
import java.util.Locale;
32+
import java.util.TimeZone;
33+
34+
import static org.junit.Assert.assertEquals;
35+
36+
/** Unit tests for {@link TimestampFromNumberAccessor} */
37+
public class TimestampFromNumberAccessorTest {
38+
39+
// An example of a calendar that observes DST.
40+
private static final Calendar LOS_ANGELES_CALENDAR =
41+
GregorianCalendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"), Locale.ROOT);
42+
43+
@Test
44+
public void testNoOffset() throws SQLException {
45+
test(
46+
1673657135052L, // UTC: 2023-01-14 00:45:23.052
47+
null,
48+
parseUtc("2023-01-14T00:45:35.052"));
49+
}
50+
51+
@Test
52+
public void testNoOffsetNanoseconds() throws SQLException {
53+
test(
54+
new BigDecimal("1673657135.052637485"), // UTC: 2023-01-14 00:45:23.052637485
55+
null,
56+
parseUtc("2023-01-14T00:45:35.052637485"));
57+
}
58+
59+
@Test
60+
public void testNoOffsetDaylightSavings() throws SQLException {
61+
test(
62+
1689320723052L, // UTC: 2023-07-14 07:45:23.052
63+
null,
64+
parseUtc("2023-07-14T07:45:23.052"));
65+
}
66+
67+
@Test
68+
public void testNoOffsetDaylightSavingsNanoseconds() throws SQLException {
69+
test(
70+
new BigDecimal("1689320723.052637485"), // UTC: 2023-07-14 07:45:23.052637485
71+
null,
72+
parseUtc("2023-07-14T07:45:23.052637485"));
73+
}
74+
75+
@Test
76+
public void testWithOffset() throws SQLException {
77+
test(
78+
1673657135052L, // UTC: 2023-01-14 00:45:23.052
79+
LOS_ANGELES_CALENDAR,
80+
parseUtc("2023-01-14T08:45:35.052"));
81+
}
82+
83+
@Test
84+
public void testWithOffsetNanoseconds() throws SQLException {
85+
test(
86+
new BigDecimal("1673657135.052637485"), // UTC: 2023-01-14 00:45:23.052637485
87+
LOS_ANGELES_CALENDAR,
88+
parseUtc("2023-01-14T08:45:35.052637485"));
89+
}
90+
91+
@Test
92+
public void testWithOffsetDaylightSavings() throws SQLException {
93+
test(
94+
1689320723052L, // UTC: 2023-07-14 07:45:23.052
95+
LOS_ANGELES_CALENDAR,
96+
parseUtc("2023-07-14T14:45:23.052"));
97+
}
98+
99+
@Test
100+
public void testWithOffsetDaylightSavingsNanoseconds() throws SQLException {
101+
test(
102+
new BigDecimal("1689320723.052637485"), // UTC: 2023-07-14 07:45:23.052637485
103+
LOS_ANGELES_CALENDAR,
104+
parseUtc("2023-07-14T14:45:23.052637485"));
105+
}
106+
107+
private static void test(Number v, Calendar calendar, Timestamp expectedValue)
108+
throws SQLException {
109+
TimestampFromNumberAccessor accessor =
110+
new TimestampFromNumberAccessor(
111+
new Getter() {
112+
@Override
113+
public Object getObject() {
114+
return v;
115+
}
116+
117+
@Override
118+
public boolean wasNull() {
119+
return v == null;
120+
}
121+
},
122+
calendar);
123+
124+
assertEquals(expectedValue, accessor.getTimestamp(calendar));
125+
assertEquals(expectedValue, accessor.getObject());
126+
}
127+
128+
private static Timestamp parseUtc(String utcTimestamp) {
129+
return Timestamp.from(LocalDateTime.parse(utcTimestamp).toInstant(ZoneOffset.UTC));
130+
}
131+
}

0 commit comments

Comments
 (0)