Skip to content

Commit 80148b7

Browse files
authored
Datetime fixes for regression (apache#36)
* Add toString to Time obj in Time#toString * Improve Time toString * Fix maven plugins * Revert "Update java/flight/flight-jdbc-driver/src/test/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessorTest.java" This reverts commit 00808c0. * Revert "Merge pull request apache#29 from rafael-telles/Timestamp_fix" This reverts commit 7924e7b, reversing changes made to f6ac593. * Fix DateTime for negative epoch * Remove unwanted change * Fix negative timestamp shift * Fix coverage * Refator DateTimeUtilsTest
1 parent 4883821 commit 80148b7

File tree

8 files changed

+398
-46
lines changed

8 files changed

+398
-46
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
18+
package org.apache.arrow.driver.jdbc;
19+
20+
import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY;
21+
22+
import java.sql.Time;
23+
import java.time.LocalTime;
24+
import java.time.temporal.ChronoField;
25+
import java.util.List;
26+
import java.util.Objects;
27+
import java.util.concurrent.TimeUnit;
28+
29+
import org.apache.arrow.util.VisibleForTesting;
30+
31+
import com.google.common.collect.ImmutableList;
32+
33+
/**
34+
* Wrapper class for Time objects to include the milliseconds part in ISO 8601 format in this#toString.
35+
*/
36+
public class ArrowFlightJdbcTime extends Time {
37+
private static final List<String> LEADING_ZEROES = ImmutableList.of("", "0", "00");
38+
39+
// Desired length of the millisecond portion should be 3
40+
private static final int DESIRED_MILLIS_LENGTH = 3;
41+
42+
// Millis of the date time object.
43+
private final int millisReprValue;
44+
45+
/**
46+
* Constructs this object based on epoch millis.
47+
*
48+
* @param milliseconds milliseconds representing Time.
49+
*/
50+
public ArrowFlightJdbcTime(final long milliseconds) {
51+
super(milliseconds);
52+
millisReprValue = getMillisReprValue(milliseconds);
53+
}
54+
55+
@VisibleForTesting
56+
ArrowFlightJdbcTime(final LocalTime time) {
57+
// Although the constructor is deprecated, this is the exact same code as Time#valueOf(LocalTime)
58+
super(time.getHour(), time.getMinute(), time.getSecond());
59+
millisReprValue = time.get(ChronoField.MILLI_OF_SECOND);
60+
}
61+
62+
private int getMillisReprValue(long milliseconds) {
63+
// Extract the millisecond part from epoch nano day
64+
if (milliseconds > MILLIS_PER_DAY) {
65+
// Convert to Epoch Day
66+
milliseconds %= MILLIS_PER_DAY;
67+
} else if (milliseconds < 0) {
68+
// LocalTime#ofNanoDay only accepts positive values
69+
milliseconds -= ((milliseconds / MILLIS_PER_DAY) - 1) * MILLIS_PER_DAY;
70+
}
71+
return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(milliseconds))
72+
.get(ChronoField.MILLI_OF_SECOND);
73+
}
74+
75+
@Override
76+
public String toString() {
77+
final StringBuilder time = new StringBuilder().append(super.toString());
78+
79+
if (millisReprValue > 0) {
80+
final String millisString = Integer.toString(millisReprValue);
81+
82+
// dot to separate the fractional seconds
83+
time.append(".");
84+
85+
final int millisLength = millisString.length();
86+
if (millisLength < DESIRED_MILLIS_LENGTH) {
87+
// add necessary leading zeroes
88+
time.append(LEADING_ZEROES.get(DESIRED_MILLIS_LENGTH - millisLength));
89+
}
90+
time.append(millisString);
91+
}
92+
93+
return time.toString();
94+
}
95+
96+
// Spotbugs requires these methods to be overridden
97+
@Override
98+
public boolean equals(Object obj) {
99+
return super.equals(obj);
100+
}
101+
102+
@Override
103+
public int hashCode() {
104+
return Objects.hash(super.hashCode(), this.millisReprValue);
105+
}
106+
}

java/flight/flight-jdbc-driver/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcDateVectorAccessor.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcDateVectorGetter.Getter;
2121
import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcDateVectorGetter.Holder;
2222
import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcDateVectorGetter.createGetter;
23+
import static org.apache.arrow.driver.jdbc.utils.DateTimeUtils.getTimestampValue;
2324
import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY;
2425
import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateToString;
2526

@@ -94,7 +95,9 @@ public Date getDate(Calendar calendar) {
9495
long value = holder.value;
9596
long milliseconds = this.timeUnit.toMillis(value);
9697

97-
return new Date(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
98+
long millisWithCalendar = DateTimeUtils.applyCalendarOffset(milliseconds, calendar);
99+
100+
return new Date(getTimestampValue(millisWithCalendar).getTime());
98101
}
99102

100103
private void fillHolder() {

java/flight/flight-jdbc-driver/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeStampVectorAccessor.java

+54-21
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.sql.Date;
2525
import java.sql.Time;
2626
import java.sql.Timestamp;
27+
import java.time.LocalDateTime;
28+
import java.time.temporal.ChronoUnit;
2729
import java.util.Calendar;
2830
import java.util.TimeZone;
2931
import java.util.concurrent.TimeUnit;
@@ -33,6 +35,7 @@
3335
import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessorFactory;
3436
import org.apache.arrow.vector.TimeStampVector;
3537
import org.apache.arrow.vector.types.pojo.ArrowType;
38+
import org.apache.arrow.vector.util.DateUtility;
3639

3740
/**
3841
* Accessor for the Arrow types extending from {@link TimeStampVector}.
@@ -42,8 +45,16 @@ public class ArrowFlightJdbcTimeStampVectorAccessor extends ArrowFlightJdbcAcces
4245
private final TimeZone timeZone;
4346
private final Getter getter;
4447
private final TimeUnit timeUnit;
48+
private final LongToLocalDateTime longToLocalDateTime;
4549
private final Holder holder;
4650

51+
/**
52+
* Functional interface used to convert a number (in any time resolution) to LocalDateTime.
53+
*/
54+
interface LongToLocalDateTime {
55+
LocalDateTime fromLong(long value);
56+
}
57+
4758
/**
4859
* Instantiate a ArrowFlightJdbcTimeStampVectorAccessor for given vector.
4960
*/
@@ -56,6 +67,7 @@ public ArrowFlightJdbcTimeStampVectorAccessor(TimeStampVector vector,
5667

5768
this.timeZone = getTimeZoneForVector(vector);
5869
this.timeUnit = getTimeUnitForVector(vector);
70+
this.longToLocalDateTime = getLongToLocalDateTimeForVector(vector, this.timeZone);
5971
}
6072

6173
@Override
@@ -68,56 +80,55 @@ public Object getObject() {
6880
return this.getTimestamp(null);
6981
}
7082

71-
private Long getLocalDateTimeMillis() {
83+
private LocalDateTime getLocalDateTime(Calendar calendar) {
7284
getter.get(getCurrentRow(), holder);
7385
this.wasNull = holder.isSet == 0;
7486
this.wasNullConsumer.setWasNull(this.wasNull);
7587
if (this.wasNull) {
7688
return null;
7789
}
7890

79-
final long value = holder.value;
80-
final long millis = this.timeUnit.toMillis(value);
91+
long value = holder.value;
92+
93+
LocalDateTime localDateTime = this.longToLocalDateTime.fromLong(value);
8194

82-
return millis + this.timeZone.getOffset(millis);
95+
if (calendar != null) {
96+
TimeZone timeZone = calendar.getTimeZone();
97+
long millis = this.timeUnit.toMillis(value);
98+
localDateTime = localDateTime
99+
.minus(timeZone.getOffset(millis) - this.timeZone.getOffset(millis), ChronoUnit.MILLIS);
100+
}
101+
return localDateTime;
83102
}
84103

85104
@Override
86105
public Date getDate(Calendar calendar) {
87-
Long millis = getLocalDateTimeMillis();
88-
if (millis == null) {
106+
LocalDateTime localDateTime = getLocalDateTime(calendar);
107+
if (localDateTime == null) {
89108
return null;
90109
}
91110

92-
return new Date(applyCalendarOffset(calendar, millis));
111+
return new Date(Timestamp.valueOf(localDateTime).getTime());
93112
}
94113

95114
@Override
96115
public Time getTime(Calendar calendar) {
97-
Long millis = getLocalDateTimeMillis();
98-
if (millis == null) {
116+
LocalDateTime localDateTime = getLocalDateTime(calendar);
117+
if (localDateTime == null) {
99118
return null;
100119
}
101120

102-
return new Time(applyCalendarOffset(calendar, millis));
121+
return new Time(Timestamp.valueOf(localDateTime).getTime());
103122
}
104123

105124
@Override
106125
public Timestamp getTimestamp(Calendar calendar) {
107-
Long millis = getLocalDateTimeMillis();
108-
if (millis == null) {
126+
LocalDateTime localDateTime = getLocalDateTime(calendar);
127+
if (localDateTime == null) {
109128
return null;
110129
}
111130

112-
return new Timestamp(applyCalendarOffset(calendar, millis));
113-
}
114-
115-
private long applyCalendarOffset(final Calendar calendar, final long millis) {
116-
if (calendar == null) {
117-
return millis - Calendar.getInstance(TimeZone.getDefault()).getTimeZone().getOffset(millis);
118-
}
119-
120-
return millis - calendar.getTimeZone().getOffset(millis) + this.timeZone.getOffset(millis);
131+
return Timestamp.valueOf(localDateTime);
121132
}
122133

123134
protected static TimeUnit getTimeUnitForVector(TimeStampVector vector) {
@@ -138,6 +149,28 @@ protected static TimeUnit getTimeUnitForVector(TimeStampVector vector) {
138149
}
139150
}
140151

152+
protected static LongToLocalDateTime getLongToLocalDateTimeForVector(TimeStampVector vector,
153+
TimeZone timeZone) {
154+
String timeZoneID = timeZone.getID();
155+
156+
ArrowType.Timestamp arrowType =
157+
(ArrowType.Timestamp) vector.getField().getFieldType().getType();
158+
159+
switch (arrowType.getUnit()) {
160+
case NANOSECOND:
161+
return nanoseconds -> DateUtility.getLocalDateTimeFromEpochNano(nanoseconds, timeZoneID);
162+
case MICROSECOND:
163+
return microseconds -> DateUtility.getLocalDateTimeFromEpochMicro(microseconds, timeZoneID);
164+
case MILLISECOND:
165+
return milliseconds -> DateUtility.getLocalDateTimeFromEpochMilli(milliseconds, timeZoneID);
166+
case SECOND:
167+
return seconds -> DateUtility.getLocalDateTimeFromEpochMilli(
168+
TimeUnit.SECONDS.toMillis(seconds), timeZoneID);
169+
default:
170+
throw new UnsupportedOperationException("Invalid Arrow time unit");
171+
}
172+
}
173+
141174
protected static TimeZone getTimeZoneForVector(TimeStampVector vector) {
142175
ArrowType.Timestamp arrowType =
143176
(ArrowType.Timestamp) vector.getField().getFieldType().getType();

java/flight/flight-jdbc-driver/src/main/java/org/apache/arrow/driver/jdbc/accessor/impl/calendar/ArrowFlightJdbcTimeVectorAccessor.java

+2-13
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeVectorGetter.Getter;
2121
import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeVectorGetter.Holder;
2222
import static org.apache.arrow.driver.jdbc.accessor.impl.calendar.ArrowFlightJdbcTimeVectorGetter.createGetter;
23-
import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY;
24-
import static org.apache.calcite.avatica.util.DateTimeUtils.unixTimeToString;
2523

2624
import java.sql.Time;
2725
import java.sql.Timestamp;
2826
import java.util.Calendar;
2927
import java.util.concurrent.TimeUnit;
3028
import java.util.function.IntSupplier;
3129

30+
import org.apache.arrow.driver.jdbc.ArrowFlightJdbcTime;
3231
import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessor;
3332
import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessorFactory;
3433
import org.apache.arrow.driver.jdbc.utils.DateTimeUtils;
@@ -126,7 +125,7 @@ public Time getTime(Calendar calendar) {
126125
long value = holder.value;
127126
long milliseconds = this.timeUnit.toMillis(value);
128127

129-
return new Time(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
128+
return new ArrowFlightJdbcTime(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
130129
}
131130

132131
private void fillHolder() {
@@ -144,16 +143,6 @@ public Timestamp getTimestamp(Calendar calendar) {
144143
return new Timestamp(time.getTime());
145144
}
146145

147-
@Override
148-
public String getString() {
149-
fillHolder();
150-
if (wasNull) {
151-
return null;
152-
}
153-
long milliseconds = timeUnit.toMillis(holder.value);
154-
return unixTimeToString((int) (milliseconds % MILLIS_PER_DAY));
155-
}
156-
157146
protected static TimeUnit getTimeUnitForVector(ValueVector vector) {
158147
if (vector instanceof TimeNanoVector) {
159148
return TimeUnit.NANOSECONDS;

java/flight/flight-jdbc-driver/src/main/java/org/apache/arrow/driver/jdbc/utils/DateTimeUtils.java

+38-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@
1717

1818
package org.apache.arrow.driver.jdbc.utils;
1919

20+
import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY;
21+
22+
import java.sql.Timestamp;
23+
import java.time.LocalDate;
24+
import java.time.LocalDateTime;
25+
import java.time.LocalTime;
2026
import java.util.Calendar;
2127
import java.util.TimeZone;
28+
import java.util.concurrent.TimeUnit;
2229

2330
/**
2431
* Datetime utility functions.
@@ -33,8 +40,37 @@ private DateTimeUtils() {
3340
*/
3441
public static long applyCalendarOffset(long milliseconds, Calendar calendar) {
3542
if (calendar == null) {
36-
return milliseconds - Calendar.getInstance(TimeZone.getDefault()).getTimeZone().getOffset(milliseconds);
43+
calendar = Calendar.getInstance();
44+
}
45+
46+
final TimeZone tz = calendar.getTimeZone();
47+
final TimeZone defaultTz = TimeZone.getDefault();
48+
49+
if (tz != defaultTz) {
50+
milliseconds -= tz.getOffset(milliseconds) - defaultTz.getOffset(milliseconds);
51+
}
52+
53+
return milliseconds;
54+
}
55+
56+
57+
/**
58+
* Converts Epoch millis to a {@link Timestamp} object.
59+
*
60+
* @param millisWithCalendar the Timestamp in Epoch millis
61+
* @return a {@link Timestamp} object representing the given Epoch millis
62+
*/
63+
public static Timestamp getTimestampValue(long millisWithCalendar) {
64+
long milliseconds = millisWithCalendar;
65+
if (milliseconds < 0) {
66+
// LocalTime#ofNanoDay only accepts positive values
67+
milliseconds -= ((milliseconds / MILLIS_PER_DAY) - 1) * MILLIS_PER_DAY;
3768
}
38-
return milliseconds - calendar.getTimeZone().getOffset(milliseconds);
69+
70+
return Timestamp.valueOf(
71+
LocalDateTime.of(
72+
LocalDate.ofEpochDay(millisWithCalendar / MILLIS_PER_DAY),
73+
LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(milliseconds % MILLIS_PER_DAY)))
74+
);
3975
}
4076
}

0 commit comments

Comments
 (0)