com.amazon.redshift.jdbc.TimestampUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of redshift-jdbc42 Show documentation
Show all versions of redshift-jdbc42 Show documentation
Java JDBC 4.2 (JRE 8+) driver for Redshift database
/*
* Copyright (c) 2003, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package com.amazon.redshift.jdbc;
import com.amazon.redshift.RedshiftStatement;
import com.amazon.redshift.core.JavaVersion;
import com.amazon.redshift.core.Oid;
import com.amazon.redshift.core.Provider;
import com.amazon.redshift.util.ByteConverter;
import com.amazon.redshift.util.GT;
import com.amazon.redshift.util.RedshiftException;
import com.amazon.redshift.util.RedshiftState;
import com.amazon.redshift.util.RedshiftTime;
import com.amazon.redshift.util.RedshiftTimestamp;
import java.lang.reflect.Field;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
//JCP! if mvn.project.property.redshift.jdbc.spec >= "JDBC4.2"
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.IsoEra;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
//JCP! endif
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
/**
* Misc utils for handling time and date values.
*/
public class TimestampUtils {
/**
* Number of milliseconds in one day.
*/
private static final int ONEDAY = 24 * 3600 * 1000;
private static final char[] ZEROS = {'0', '0', '0', '0', '0', '0', '0', '0', '0'};
private static final char[][] NUMBERS;
private static final HashMap GMT_ZONES = new HashMap();
private static final int MAX_NANOS_BEFORE_WRAP_ON_ROUND = 999999500;
//JCP! if mvn.project.property.redshift.jdbc.spec >= "JDBC4.2"
private static final Duration ONE_MICROSECOND = Duration.ofNanos(1000);
// LocalTime.MAX is 23:59:59.999_999_999, and it wraps to 24:00:00 when nanos exceed 999_999_499
// since Redshift has microsecond resolution only
private static final LocalTime MAX_TIME = LocalTime.MAX.minus(Duration.ofNanos(500));
private static final OffsetDateTime MAX_OFFSET_DATETIME = OffsetDateTime.MAX.minus(Duration.ofMillis(500));
private static final LocalDateTime MAX_LOCAL_DATETIME = LocalDateTime.MAX.minus(Duration.ofMillis(500));
// low value for dates is 4713 BC
private static final LocalDate MIN_LOCAL_DATE = LocalDate.of(4713, 1, 1).with(ChronoField.ERA, IsoEra.BCE.getValue());
private static final LocalDateTime MIN_LOCAL_DATETIME = MIN_LOCAL_DATE.atStartOfDay();
private static final OffsetDateTime MIN_OFFSET_DATETIME = MIN_LOCAL_DATETIME.atOffset(ZoneOffset.UTC);
//JCP! endif
private static final Field DEFAULT_TIME_ZONE_FIELD;
private TimeZone prevDefaultZoneFieldValue;
private TimeZone defaultTimeZoneCache;
static {
// The expected maximum value is 60 (seconds), so 64 is used "just in case"
NUMBERS = new char[64][];
for (int i = 0; i < NUMBERS.length; i++) {
NUMBERS[i] = ((i < 10 ? "0" : "") + Integer.toString(i)).toCharArray();
}
// Backend's gmt-3 means GMT+03 in Java. Here a map is created so gmt-3 can be converted to
// java TimeZone
for (int i = -12; i <= 14; i++) {
TimeZone timeZone;
String pgZoneName;
if (i == 0) {
timeZone = TimeZone.getTimeZone("GMT");
pgZoneName = "GMT";
} else {
timeZone = TimeZone.getTimeZone("GMT" + (i <= 0 ? "+" : "-") + Math.abs(i));
pgZoneName = "GMT" + (i >= 0 ? "+" : "-");
}
if (i == 0) {
GMT_ZONES.put(pgZoneName, timeZone);
continue;
}
GMT_ZONES.put(pgZoneName + Math.abs(i), timeZone);
GMT_ZONES.put(pgZoneName + new String(NUMBERS[Math.abs(i)]), timeZone);
}
// Fast path to getting the default timezone.
// Accessing the default timezone over and over creates a clone with regular API.
// Because we don't mutate that object in our use of it, we can access the field directly.
// This saves the creation of a clone everytime, and the memory associated to all these clones.
Field tzField;
try {
tzField = null;
// Avoid reflective access in Java 9+
if (JavaVersion.getRuntimeVersion().compareTo(JavaVersion.v1_8) <= 0) {
tzField = TimeZone.class.getDeclaredField("defaultTimeZone");
tzField.setAccessible(true);
TimeZone defaultTz = TimeZone.getDefault();
Object tzFromField = tzField.get(null);
if (defaultTz == null || !defaultTz.equals(tzFromField)) {
tzField = null;
}
}
} catch (Exception e) {
tzField = null;
}
DEFAULT_TIME_ZONE_FIELD = tzField;
}
private final StringBuilder sbuf = new StringBuilder();
// This calendar is used when user provides calendar in setX(, Calendar) method.
// It ensures calendar is Gregorian.
private final Calendar calendarWithUserTz = new GregorianCalendar();
private final TimeZone utcTz = TimeZone.getTimeZone("UTC");
private Calendar calCache;
private int calCacheZone;
/**
* True if the backend uses doubles for time values. False if long is used.
*/
private final boolean usesDouble;
private final Provider timeZoneProvider;
TimestampUtils(boolean usesDouble, Provider timeZoneProvider) {
this.usesDouble = usesDouble;
this.timeZoneProvider = timeZoneProvider;
}
private Calendar getCalendar(int sign, int hr, int min, int sec) {
int rawOffset = sign * (((hr * 60 + min) * 60 + sec) * 1000);
if (calCache != null && calCacheZone == rawOffset) {
return calCache;
}
StringBuilder zoneID = new StringBuilder("GMT");
zoneID.append(sign < 0 ? '-' : '+');
if (hr < 10) {
zoneID.append('0');
}
zoneID.append(hr);
if (min < 10) {
zoneID.append('0');
}
zoneID.append(min);
if (sec < 10) {
zoneID.append('0');
}
zoneID.append(sec);
TimeZone syntheticTZ = new SimpleTimeZone(rawOffset, zoneID.toString());
calCache = new GregorianCalendar(syntheticTZ);
calCacheZone = rawOffset;
return calCache;
}
private static class ParsedTimestamp {
boolean hasDate = false;
int era = GregorianCalendar.AD;
int year = 1970;
int month = 1;
boolean hasTime = false;
int day = 1;
int hour = 0;
int minute = 0;
int second = 0;
int nanos = 0;
Calendar tz = null;
}
private static class ParsedBinaryTimestamp {
Infinity infinity = null;
long millis = 0;
int nanos = 0;
}
enum Infinity {
POSITIVE,
NEGATIVE;
}
/**
* Load date/time information into the provided calendar returning the fractional seconds.
*/
private ParsedTimestamp parseBackendTimestamp(String str) throws SQLException {
char[] s = str.toCharArray();
int slen = s.length;
// This is pretty gross..
ParsedTimestamp result = new ParsedTimestamp();
// We try to parse these fields in order; all are optional
// (but some combinations don't make sense, e.g. if you have
// both date and time then they must be whitespace-separated).
// At least one of date and time must be present.
// leading whitespace
// yyyy-mm-dd
// whitespace
// hh:mm:ss
// whitespace
// timezone in one of the formats: +hh, -hh, +hh:mm, -hh:mm
// whitespace
// if date is present, an era specifier: AD or BC
// trailing whitespace
try {
int start = skipWhitespace(s, 0); // Skip leading whitespace
int end = firstNonDigit(s, start);
int num;
char sep;
// Possibly read date.
if (charAt(s, end) == '-') {
//
// Date
//
result.hasDate = true;
// year
result.year = number(s, start, end);
start = end + 1; // Skip '-'
// month
end = firstNonDigit(s, start);
result.month = number(s, start, end);
sep = charAt(s, end);
if (sep != '-') {
throw new NumberFormatException("Expected date to be dash-separated, got '" + sep + "'");
}
start = end + 1; // Skip '-'
// day of month
end = firstNonDigit(s, start);
result.day = number(s, start, end);
start = skipWhitespace(s, end); // Skip trailing whitespace
}
// Possibly read time.
if (Character.isDigit(charAt(s, start))) {
//
// Time.
//
result.hasTime = true;
// Hours
end = firstNonDigit(s, start);
result.hour = number(s, start, end);
sep = charAt(s, end);
if (sep != ':') {
throw new NumberFormatException("Expected time to be colon-separated, got '" + sep + "'");
}
start = end + 1; // Skip ':'
// minutes
end = firstNonDigit(s, start);
result.minute = number(s, start, end);
sep = charAt(s, end);
if (sep != ':') {
throw new NumberFormatException("Expected time to be colon-separated, got '" + sep + "'");
}
start = end + 1; // Skip ':'
// seconds
end = firstNonDigit(s, start);
result.second = number(s, start, end);
start = end;
// Fractional seconds.
if (charAt(s, start) == '.') {
end = firstNonDigit(s, start + 1); // Skip '.'
num = number(s, start + 1, end);
for (int numlength = (end - (start + 1)); numlength < 9; ++numlength) {
num *= 10;
}
result.nanos = num;
start = end;
}
start = skipWhitespace(s, start); // Skip trailing whitespace
}
// Possibly read timezone.
sep = charAt(s, start);
if (sep == '-' || sep == '+') {
int tzsign = (sep == '-') ? -1 : 1;
int tzhr;
int tzmin;
int tzsec;
end = firstNonDigit(s, start + 1); // Skip +/-
tzhr = number(s, start + 1, end);
start = end;
sep = charAt(s, start);
if (sep == ':') {
end = firstNonDigit(s, start + 1); // Skip ':'
tzmin = number(s, start + 1, end);
start = end;
} else {
tzmin = 0;
}
tzsec = 0;
sep = charAt(s, start);
if (sep == ':') {
end = firstNonDigit(s, start + 1); // Skip ':'
tzsec = number(s, start + 1, end);
start = end;
}
// Setting offset does not seem to work correctly in all
// cases.. So get a fresh calendar for a synthetic timezone
// instead
result.tz = getCalendar(tzsign, tzhr, tzmin, tzsec);
start = skipWhitespace(s, start); // Skip trailing whitespace
}
if (result.hasDate && start < slen) {
String eraString = new String(s, start, slen - start);
if (eraString.startsWith("AD")) {
result.era = GregorianCalendar.AD;
start += 2;
} else if (eraString.startsWith("BC")) {
result.era = GregorianCalendar.BC;
start += 2;
}
}
if (start < slen) {
throw new NumberFormatException(
"Trailing junk on timestamp: '" + new String(s, start, slen - start) + "'");
}
if (!result.hasTime && !result.hasDate) {
throw new NumberFormatException("Timestamp has neither date nor time");
}
} catch (NumberFormatException nfe) {
throw new RedshiftException(
GT.tr("Bad value for type timestamp/date/time: {1}", str),
RedshiftState.BAD_DATETIME_FORMAT, nfe);
}
return result;
}
/**
* Parse a string and return a timestamp representing its value.
*
* @param cal calendar to be used to parse the input string
* @param s The ISO formated date string to parse.
* @return null if s is null or a timestamp of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public synchronized Timestamp toTimestamp(Calendar cal, String s) throws SQLException {
if (s == null) {
return null;
}
int slen = s.length();
// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && s.equals("infinity")) {
return new Timestamp(RedshiftStatement.DATE_POSITIVE_INFINITY);
}
if (slen == 9 && s.equals("-infinity")) {
return new Timestamp(RedshiftStatement.DATE_NEGATIVE_INFINITY);
}
ParsedTimestamp ts = parseBackendTimestamp(s);
if(ts.month < 1 || ts.month > 12)
{
throw new SQLException("Invalid value for month in timestamp : " + ts.month);
}
if(ts.day < 1 || ts.day > 31)
{
throw new SQLException("Invalid value for day of month in timestamp : " + ts.day);
}
if(ts.hour < 0 || ts.hour > 24)
{
throw new SQLException("Invalid value for hour of month in timestamp : " + ts.hour);
}
if(ts.minute < 0 || ts.minute > 60)
{
throw new SQLException("Invalid value for minute in timestamp : " + ts.minute);
}
if(ts.second < 0 || ts.second > 60)
{
throw new SQLException("Invalid value for second in timestamp : " + ts.second);
}
Calendar useCal = ts.tz != null ? ts.tz : setupCalendar(cal);
useCal.set(Calendar.ERA, ts.era);
useCal.set(Calendar.YEAR, ts.year);
useCal.set(Calendar.MONTH, ts.month - 1);
useCal.set(Calendar.DAY_OF_MONTH, ts.day);
useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
useCal.set(Calendar.MINUTE, ts.minute);
useCal.set(Calendar.SECOND, ts.second);
useCal.set(Calendar.MILLISECOND, 0);
Timestamp result;
if(ts.tz != null) {
result = new RedshiftTimestamp(useCal.getTimeInMillis(), useCal, s);
}
else
result = new Timestamp(useCal.getTimeInMillis());
result.setNanos(ts.nanos);
return result;
}
//JCP! if mvn.project.property.redshift.jdbc.spec >= "JDBC4.2"
/**
* Parse a string and return a LocalTime representing its value.
*
* @param s The ISO formated time string to parse.
* @return null if s is null or a LocalTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public LocalTime toLocalTime(String s) throws SQLException {
if (s == null) {
return null;
}
if (s.equals("24:00:00")) {
return LocalTime.MAX;
}
try {
return LocalTime.parse(s);
} catch (DateTimeParseException nfe) {
throw new RedshiftException(
GT.tr("Bad value for type timestamp/date/time: {1}", s),
RedshiftState.BAD_DATETIME_FORMAT, nfe);
}
}
/**
* Parse a string and return a LocalDateTime representing its value.
*
* @param s The ISO formated date string to parse.
* @return null if s is null or a LocalDateTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public LocalDateTime toLocalDateTime(String s) throws SQLException {
if (s == null) {
return null;
}
int slen = s.length();
// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && s.equals("infinity")) {
return LocalDateTime.MAX;
}
if (slen == 9 && s.equals("-infinity")) {
return LocalDateTime.MIN;
}
ParsedTimestamp ts = parseBackendTimestamp(s);
// intentionally ignore time zone
// 2004-10-19 10:23:54+03:00 is 2004-10-19 10:23:54 locally
LocalDateTime result = LocalDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos);
if (ts.era == GregorianCalendar.BC) {
return result.with(ChronoField.ERA, IsoEra.BCE.getValue());
} else {
return result;
}
}
/**
* Parse a string and return a LocalDateTime representing its value.
*
* @param s The ISO formated date string to parse.
* @return null if s is null or a LocalDateTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public OffsetDateTime toOffsetDateTime(String s) throws SQLException {
if (s == null) {
return null;
}
int slen = s.length();
// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && s.equals("infinity")) {
return OffsetDateTime.MAX;
}
if (slen == 9 && s.equals("-infinity")) {
return OffsetDateTime.MIN;
}
ParsedTimestamp ts = parseBackendTimestamp(s);
Calendar tz = ts.tz;
int offsetSeconds;
if (tz == null) {
offsetSeconds = 0;
} else {
offsetSeconds = tz.get(Calendar.ZONE_OFFSET) / 1000;
}
ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(offsetSeconds);
// Postgres is always UTC
OffsetDateTime result = OffsetDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos, zoneOffset)
.withOffsetSameInstant(ZoneOffset.UTC);
if (ts.era == GregorianCalendar.BC) {
return result.with(ChronoField.ERA, IsoEra.BCE.getValue());
} else {
return result;
}
}
/**
* Returns the offset date time object matching the given bytes with Oid#TIMETZ.
*
* @param t the time value
* @return the matching offset date time
*/
public OffsetDateTime toOffsetDateTime(Time t) {
// hardcode utc because the backend does not provide us the timezone
// hardoce UNIX epoch, JDBC requires OffsetDateTime but doesn't describe what date should be used
return t.toLocalTime().atDate(LocalDate.of(1970, 1, 1)).atOffset(ZoneOffset.UTC);
}
/**
* Returns the offset date time object matching the given bytes with Oid#TIMESTAMPTZ.
*
* @param bytes The binary encoded local date time value.
* @return The parsed local date time object.
* @throws RedshiftException If binary format could not be parsed.
*/
public OffsetDateTime toOffsetDateTimeBin(byte[] bytes) throws RedshiftException {
ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return OffsetDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return OffsetDateTime.MIN;
}
// hardcode utc because the backend does not provide us the timezone
// Postgres is always UTC
Instant instant = Instant.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos);
return OffsetDateTime.ofInstant(instant, ZoneOffset.UTC);
}
//JCP! endif
public synchronized Time toTime(Calendar cal, String s) throws SQLException {
// 1) Parse backend string
if (s == null) {
return null;
}
ParsedTimestamp ts = parseBackendTimestamp(s);
Calendar useCal = ts.tz != null ? ts.tz : setupCalendar(cal);
if (ts.tz == null) {
// When no time zone provided (e.g. time or timestamp)
// We get the year-month-day from the string, then truncate the day to 1970-01-01
// This is used for timestamp -> time conversion
// Note: this cannot be merged with "else" branch since
// timestamps at which the time flips to/from DST depend on the date
// For instance, 2000-03-26 02:00:00 is invalid timestamp in Europe/Moscow time zone
// and the valid one is 2000-03-26 03:00:00. That is why we parse full timestamp
// then set year to 1970 later
useCal.set(Calendar.ERA, ts.era);
useCal.set(Calendar.YEAR, ts.year);
useCal.set(Calendar.MONTH, ts.month - 1);
useCal.set(Calendar.DAY_OF_MONTH, ts.day);
} else {
// When time zone is given, we just pick the time part and assume date to be 1970-01-01
// this is used for time, timez, and timestamptz parsing
useCal.set(Calendar.ERA, GregorianCalendar.AD);
useCal.set(Calendar.YEAR, 1970);
useCal.set(Calendar.MONTH, Calendar.JANUARY);
useCal.set(Calendar.DAY_OF_MONTH, 1);
}
useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
useCal.set(Calendar.MINUTE, ts.minute);
useCal.set(Calendar.SECOND, ts.second);
useCal.set(Calendar.MILLISECOND, 0);
long timeMillis = useCal.getTimeInMillis() + ts.nanos / 1000000;
if (ts.tz != null || (ts.year == 1970 && ts.era == GregorianCalendar.AD)) {
// time with time zone has proper time zone, so the value can be returned as is
return new Time(timeMillis);
}
// 2) Truncate date part so in given time zone the date would be formatted as 01/01/1970
return convertToTime(timeMillis, useCal.getTimeZone());
}
public synchronized Date toDate(Calendar cal, String s) throws SQLException {
// 1) Parse backend string
Timestamp timestamp = toTimestamp(cal, s);
if (timestamp == null) {
return null;
}
// Note: infinite dates are handled in convertToDate
// 2) Truncate date part so in given time zone the date would be formatted as 00:00
return convertToDate(timestamp.getTime(), cal == null ? null : cal.getTimeZone());
}
private Calendar setupCalendar(Calendar cal) {
TimeZone timeZone = cal == null ? null : cal.getTimeZone();
return getSharedCalendar(timeZone);
}
/**
* Get a shared calendar, applying the supplied time zone or the default time zone if null.
*
* @param timeZone time zone to be set for the calendar
* @return The shared calendar.
*/
public Calendar getSharedCalendar(TimeZone timeZone) {
if (timeZone == null) {
timeZone = getDefaultTz();
}
Calendar tmp = calendarWithUserTz;
tmp.setTimeZone(timeZone);
return tmp;
}
/**
* Returns true when microsecond part of the time should be increased
* when rounding to microseconds
* @param nanos nanosecond part of the time
* @return true when microsecond part of the time should be increased when rounding to microseconds
*/
private static boolean nanosExceed499(int nanos) {
return nanos % 1000 > 499;
}
public synchronized String toString(Calendar cal, Timestamp x) {
return toString(cal, x, true);
}
public synchronized String toString(Calendar cal, Timestamp x,
boolean withTimeZone) {
if (x.getTime() == RedshiftStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (x.getTime() == RedshiftStatement.DATE_NEGATIVE_INFINITY) {
return "-infinity";
}
cal = setupCalendar(cal);
long timeMillis = x.getTime();
// Round to microseconds
int nanos = x.getNanos();
if (nanos >= MAX_NANOS_BEFORE_WRAP_ON_ROUND) {
nanos = 0;
timeMillis++;
} else if (nanosExceed499(nanos)) {
// PostgreSQL does not support nanosecond resolution yet, and appendTime will just ignore
// 0..999 part of the nanoseconds, however we subtract nanos % 1000 to make the value
// a little bit saner for debugging reasons
nanos += 1000 - nanos % 1000;
}
cal.setTimeInMillis(timeMillis);
sbuf.setLength(0);
appendDate(sbuf, cal);
sbuf.append(' ');
appendTime(sbuf, cal, nanos);
if (withTimeZone) {
appendTimeZone(sbuf, cal);
}
appendEra(sbuf, cal);
return sbuf.toString();
}
public synchronized String toString(Calendar cal, Date x) {
return toString(cal, x, true);
}
public synchronized String toString(Calendar cal, Date x,
boolean withTimeZone) {
if (x.getTime() == RedshiftStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (x.getTime() == RedshiftStatement.DATE_NEGATIVE_INFINITY) {
return "-infinity";
}
cal = setupCalendar(cal);
cal.setTime(x);
sbuf.setLength(0);
appendDate(sbuf, cal);
appendEra(sbuf, cal);
if (withTimeZone) {
sbuf.append(' ');
appendTimeZone(sbuf, cal);
}
return sbuf.toString();
}
public synchronized String toString(Calendar cal, Time x) {
return toString(cal, x, true);
}
public synchronized String toString(Calendar cal, Time x,
boolean withTimeZone) {
cal = setupCalendar(cal);
cal.setTime(x);
sbuf.setLength(0);
int nanos;
if(x instanceof RedshiftTime)
nanos = ((RedshiftTime)x).getNanos();
else
nanos = cal.get(Calendar.MILLISECOND) * 1000000;
appendTime(sbuf, cal, nanos);
// The 'time' parser for <= 7.3 doesn't like timezones.
if (withTimeZone) {
appendTimeZone(sbuf, cal);
}
return sbuf.toString();
}
private static void appendDate(StringBuilder sb, Calendar cal) {
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1;
int day = cal.get(Calendar.DAY_OF_MONTH);
appendDate(sb, year, month, day);
}
private static void appendDate(StringBuilder sb, int year, int month, int day) {
// always use at least four digits for the year so very
// early years, like 2, don't get misinterpreted
//
int prevLength = sb.length();
sb.append(year);
int leadingZerosForYear = 4 - (sb.length() - prevLength);
if (leadingZerosForYear > 0) {
sb.insert(prevLength, ZEROS, 0, leadingZerosForYear);
}
sb.append('-');
sb.append(NUMBERS[month]);
sb.append('-');
sb.append(NUMBERS[day]);
}
private static void appendTime(StringBuilder sb, Calendar cal, int nanos) {
int hours = cal.get(Calendar.HOUR_OF_DAY);
int minutes = cal.get(Calendar.MINUTE);
int seconds = cal.get(Calendar.SECOND);
appendTime(sb, hours, minutes, seconds, nanos);
}
/**
* Appends time part to the {@code StringBuilder} in Redshift-compatible format.
* The function truncates {@param nanos} to microseconds. The value is expected to be rounded
* beforehand.
* @param sb destination
* @param hours hours
* @param minutes minutes
* @param seconds seconds
* @param nanos nanoseconds
*/
private static void appendTime(StringBuilder sb, int hours, int minutes, int seconds, int nanos) {
sb.append(NUMBERS[hours]);
sb.append(':');
sb.append(NUMBERS[minutes]);
sb.append(':');
sb.append(NUMBERS[seconds]);
// Add nanoseconds.
// This won't work for server versions < 7.2 which only want
// a two digit fractional second, but we don't need to support 7.1
// anymore and getting the version number here is difficult.
//
if (nanos < 1000) {
return;
}
sb.append('.');
int len = sb.length();
sb.append(nanos / 1000); // append microseconds
int needZeros = 6 - (sb.length() - len);
if (needZeros > 0) {
sb.insert(len, ZEROS, 0, needZeros);
}
int end = sb.length() - 1;
while (sb.charAt(end) == '0') {
sb.deleteCharAt(end);
end--;
}
}
private void appendTimeZone(StringBuilder sb, java.util.Calendar cal) {
int offset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000;
appendTimeZone(sb, offset);
}
private void appendTimeZone(StringBuilder sb, int offset) {
int absoff = Math.abs(offset);
int hours = absoff / 60 / 60;
int mins = (absoff - hours * 60 * 60) / 60;
int secs = absoff - hours * 60 * 60 - mins * 60;
sb.append((offset >= 0) ? "+" : "-");
sb.append(NUMBERS[hours]);
if (mins == 0 && secs == 0) {
return;
}
sb.append(':');
sb.append(NUMBERS[mins]);
if (secs != 0) {
sb.append(':');
sb.append(NUMBERS[secs]);
}
}
private static void appendEra(StringBuilder sb, Calendar cal) {
if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
sb.append(" BC");
}
}
//JCP! if mvn.project.property.redshift.jdbc.spec >= "JDBC4.2"
public synchronized String toString(LocalDate localDate) {
if (LocalDate.MAX.equals(localDate)) {
return "infinity";
} else if (localDate.isBefore(MIN_LOCAL_DATE)) {
return "-infinity";
}
sbuf.setLength(0);
appendDate(sbuf, localDate);
appendEra(sbuf, localDate);
return sbuf.toString();
}
public synchronized String toString(LocalTime localTime) {
sbuf.setLength(0);
if (localTime.isAfter(MAX_TIME)) {
return "24:00:00";
}
int nano = localTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
localTime = localTime.plus(ONE_MICROSECOND);
}
appendTime(sbuf, localTime);
return sbuf.toString();
}
public synchronized String toString(OffsetDateTime offsetDateTime) {
if (offsetDateTime.isAfter(MAX_OFFSET_DATETIME)) {
return "infinity";
} else if (offsetDateTime.isBefore(MIN_OFFSET_DATETIME)) {
return "-infinity";
}
sbuf.setLength(0);
int nano = offsetDateTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
offsetDateTime = offsetDateTime.plus(ONE_MICROSECOND);
}
LocalDateTime localDateTime = offsetDateTime.toLocalDateTime();
LocalDate localDate = localDateTime.toLocalDate();
appendDate(sbuf, localDate);
sbuf.append(' ');
appendTime(sbuf, localDateTime.toLocalTime());
appendTimeZone(sbuf, offsetDateTime.getOffset());
appendEra(sbuf, localDate);
return sbuf.toString();
}
/**
* Formats {@link LocalDateTime} to be sent to the backend, thus it adds time zone.
* Do not use this method in {@link java.sql.ResultSet#getString(int)}
* @param localDateTime The local date to format as a String
* @return The formatted local date
*/
public synchronized String toString(LocalDateTime localDateTime) {
if (localDateTime.isAfter(MAX_LOCAL_DATETIME)) {
return "infinity";
} else if (localDateTime.isBefore(MIN_LOCAL_DATETIME)) {
return "-infinity";
}
// LocalDateTime is always passed with time zone so backend can decide between timestamp and timestamptz
ZonedDateTime zonedDateTime = localDateTime.atZone(getDefaultTz().toZoneId());
return toString(zonedDateTime.toOffsetDateTime());
}
private static void appendDate(StringBuilder sb, LocalDate localDate) {
int year = localDate.get(ChronoField.YEAR_OF_ERA);
int month = localDate.getMonthValue();
int day = localDate.getDayOfMonth();
appendDate(sb, year, month, day);
}
private static void appendTime(StringBuilder sb, LocalTime localTime) {
int hours = localTime.getHour();
int minutes = localTime.getMinute();
int seconds = localTime.getSecond();
int nanos = localTime.getNano();
appendTime(sb, hours, minutes, seconds, nanos);
}
private void appendTimeZone(StringBuilder sb, ZoneOffset offset) {
int offsetSeconds = offset.getTotalSeconds();
appendTimeZone(sb, offsetSeconds);
}
private static void appendEra(StringBuilder sb, LocalDate localDate) {
if (localDate.get(ChronoField.ERA) == IsoEra.BCE.getValue()) {
sb.append(" BC");
}
}
//JCP! endif
private static int skipWhitespace(char[] s, int start) {
int slen = s.length;
for (int i = start; i < slen; i++) {
if (!Character.isSpace(s[i])) {
return i;
}
}
return slen;
}
private static int firstNonDigit(char[] s, int start) {
int slen = s.length;
for (int i = start; i < slen; i++) {
if (!Character.isDigit(s[i])) {
return i;
}
}
return slen;
}
private static int number(char[] s, int start, int end) {
if (start >= end) {
throw new NumberFormatException();
}
int n = 0;
for (int i = start; i < end; i++) {
n = 10 * n + (s[i] - '0');
}
return n;
}
private static char charAt(char[] s, int pos) {
if (pos >= 0 && pos < s.length) {
return s[pos];
}
return '\0';
}
/**
* Returns the SQL Date object matching the given bytes with {@link Oid#DATE}.
*
* @param tz The timezone used.
* @param bytes The binary encoded date value.
* @return The parsed date object.
* @throws RedshiftException If binary format could not be parsed.
*/
public Date toDateBin(TimeZone tz, byte[] bytes) throws RedshiftException {
if (bytes.length != 4) {
throw new RedshiftException(GT.tr("Unsupported binary encoding of {0}.", "date"),
RedshiftState.BAD_DATETIME_FORMAT);
}
int days = ByteConverter.int4(bytes, 0);
if (tz == null) {
tz = getDefaultTz();
}
long secs = toJavaSecs(days * 86400L);
long millis = secs * 1000L;
if (millis <= RedshiftStatement.DATE_NEGATIVE_SMALLER_INFINITY) {
millis = RedshiftStatement.DATE_NEGATIVE_INFINITY;
} else if (millis >= RedshiftStatement.DATE_POSITIVE_SMALLER_INFINITY) {
millis = RedshiftStatement.DATE_POSITIVE_INFINITY;
} else {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
}
return new Date(millis);
}
private TimeZone getDefaultTz() {
// Fast path to getting the default timezone.
if (DEFAULT_TIME_ZONE_FIELD != null) {
try {
TimeZone defaultTimeZone = (TimeZone) DEFAULT_TIME_ZONE_FIELD.get(null);
if (defaultTimeZone == prevDefaultZoneFieldValue) {
return defaultTimeZoneCache;
}
prevDefaultZoneFieldValue = defaultTimeZone;
} catch (Exception e) {
// If this were to fail, fallback on slow method.
}
}
TimeZone tz = TimeZone.getDefault();
defaultTimeZoneCache = tz;
return tz;
}
public boolean hasFastDefaultTimeZone() {
return DEFAULT_TIME_ZONE_FIELD != null;
}
/**
* Returns the SQL Time object matching the given bytes with {@link Oid#TIME} or
* {@link Oid#TIMETZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIME}, ignored if data already
* contains {@link Oid#TIMETZ}.
* @param bytes The binary encoded time value.
* @return The parsed time object.
* @throws RedshiftException If binary format could not be parsed.
*/
public Time toTimeBin(TimeZone tz, byte[] bytes) throws RedshiftException {
if ((bytes.length != 8 && bytes.length != 12)) {
throw new RedshiftException(GT.tr("Unsupported binary encoding of {0}.", "time"),
RedshiftState.BAD_DATETIME_FORMAT);
}
long millis;
int timeOffset;
int nanos = 0;
Time timeObj;
if (usesDouble) {
double time = ByteConverter.float8(bytes, 0);
millis = (long) (time * 1000);
} else {
long time = ByteConverter.int8(bytes, 0);
millis = time / 1000;
if ((time % 1000) > 0) {
// There is a microsec fraction. Server sends precision upto Micro only.
nanos = (int)(time % 1000000)*1000;
}
}
if (bytes.length == 12) {
timeOffset = ByteConverter.int4(bytes, 8);
timeOffset *= -1000;
millis -= timeOffset;
timeObj = new Time(millis);
return (nanos > 0) ? new RedshiftTime(timeObj, nanos) : timeObj;
}
if (tz == null) {
tz = getDefaultTz();
}
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
timeObj = convertToTime(millis, tz); // Ensure date part is 1970-01-01
return (nanos > 0) ? new RedshiftTime(timeObj, nanos) : timeObj;
}
//JCP! if mvn.project.property.redshift.jdbc.spec >= "JDBC4.2"
/**
* Returns the SQL Time object matching the given bytes with {@link Oid#TIME}.
*
* @param bytes The binary encoded time value.
* @return The parsed time object.
* @throws RedshiftException If binary format could not be parsed.
*/
public LocalTime toLocalTimeBin(byte[] bytes) throws RedshiftException {
if (bytes.length != 8) {
throw new RedshiftException(GT.tr("Unsupported binary encoding of {0}.", "time"),
RedshiftState.BAD_DATETIME_FORMAT);
}
long micros;
if (usesDouble) {
double seconds = ByteConverter.float8(bytes, 0);
micros = (long) (seconds * 1000000d);
} else {
micros = ByteConverter.int8(bytes, 0);
}
return LocalTime.ofNanoOfDay(micros * 1000);
}
//JCP! endif
/**
* Returns the SQL Timestamp object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIMESTAMP}, ignored if data
* already contains {@link Oid#TIMESTAMPTZ}.
* @param bytes The binary encoded timestamp value.
* @param timestamptz True if the binary is in GMT.
* @param cal Calendar to use
* @return The parsed timestamp object.
* @throws RedshiftException If binary format could not be parsed.
*/
public Timestamp toTimestampBin(TimeZone tz, byte[] bytes, boolean timestamptz,java.util.Calendar cal)
throws RedshiftException {
ParsedBinaryTimestamp parsedTimestamp = this.toParsedTimestampBin(tz, bytes, timestamptz);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return new Timestamp(RedshiftStatement.DATE_POSITIVE_INFINITY);
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return new Timestamp(RedshiftStatement.DATE_NEGATIVE_INFINITY);
}
Timestamp ts;
if(timestamptz) {
ts = new RedshiftTimestamp(parsedTimestamp.millis, cal);
} else {
ts = new Timestamp(parsedTimestamp.millis);
}
ts.setNanos(parsedTimestamp.nanos);
return ts;
}
/**
* Returns the SQL Timestamp object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIMESTAMP}, ignored if data
* already contains {@link Oid#TIMESTAMPTZ}.
* @param bytes The binary encoded timestamp value of ABSTIME. ABSTIME has 4 bytes.
* @param timestamptz True if the binary is in GMT.
* @param cal Calendar to use
* @return The parsed timestamp object.
* @throws RedshiftException If binary format could not be parsed.
*/
public Timestamp toTimestampAbsTimeBin(TimeZone tz, byte[] bytes, boolean timestamptz,java.util.Calendar cal)
throws RedshiftException {
ParsedBinaryTimestamp parsedTimestamp = this.toParsedTimestampAbsTimeBin(tz, bytes, timestamptz);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return new Timestamp(RedshiftStatement.DATE_POSITIVE_INFINITY);
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return new Timestamp(RedshiftStatement.DATE_NEGATIVE_INFINITY);
}
Timestamp ts;
if(timestamptz) {
ts = new RedshiftTimestamp(parsedTimestamp.millis, cal);
} else {
ts = new Timestamp(parsedTimestamp.millis);
}
ts.setNanos(parsedTimestamp.nanos);
return ts;
}
private ParsedBinaryTimestamp toParsedTimestampBinPlain(byte[] bytes)
throws RedshiftException {
if (bytes.length != 8) {
throw new RedshiftException(GT.tr("Unsupported binary encoding of {0}.", "timestamp"),
RedshiftState.BAD_DATETIME_FORMAT);
}
long secs;
int nanos;
if (usesDouble) {
double time = ByteConverter.float8(bytes, 0);
if (time == Double.POSITIVE_INFINITY) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.POSITIVE;
return ts;
} else if (time == Double.NEGATIVE_INFINITY) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.NEGATIVE;
return ts;
}
secs = (long) time;
nanos = (int) ((time - secs) * 1000000);
} else {
long time = ByteConverter.int8(bytes, 0);
// compatibility with text based receiving, not strictly necessary
// and can actually be confusing because there are timestamps
// that are larger than infinite
if (time == Long.MAX_VALUE) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.POSITIVE;
return ts;
} else if (time == Long.MIN_VALUE) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.NEGATIVE;
return ts;
}
secs = time / 1000000;
nanos = (int) (time - secs * 1000000);
}
if (nanos < 0) {
secs--;
nanos += 1000000;
}
nanos *= 1000;
long millis = secs * 1000L;
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.millis = millis;
ts.nanos = nanos;
return ts;
}
// Restricted ABSTIME is 4bytes.
private ParsedBinaryTimestamp toParsedTimestampBinAbsTimePlain(byte[] bytes)
throws RedshiftException {
if (bytes.length != 4) {
throw new RedshiftException(GT.tr("Unsupported binary encoding of {0}.", "abstime"),
RedshiftState.BAD_DATETIME_FORMAT);
}
long secs;
int nanos;
if (usesDouble) {
double time = ByteConverter.float4(bytes, 0);
if (time == Double.POSITIVE_INFINITY) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.POSITIVE;
return ts;
} else if (time == Double.NEGATIVE_INFINITY) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.NEGATIVE;
return ts;
}
secs = (long) time;
nanos = (int) ((time - secs) * 1000000);
} else {
long time = ByteConverter.int4(bytes, 0); // Time in secs
time *= 1000000; // Time in micro secs
// compatibility with text based receiving, not strictly necessary
// and can actually be confusing because there are timestamps
// that are larger than infinite
if (time == Long.MAX_VALUE) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.POSITIVE;
return ts;
} else if (time == Long.MIN_VALUE) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.NEGATIVE;
return ts;
}
secs = time / 1000000;
nanos = (int) (time - secs * 1000000);
}
if (nanos < 0) {
secs--;
nanos += 1000000;
}
nanos *= 1000;
long millis = secs * 1000L;
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.millis = millis;
ts.nanos = nanos;
return ts;
}
private ParsedBinaryTimestamp toParsedTimestampBin(TimeZone tz, byte[] bytes, boolean timestamptz)
throws RedshiftException {
ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes);
if (ts.infinity != null) {
return ts;
}
long secs = ts.millis / 1000L;
secs = toJavaSecs(secs);
long millis = secs * 1000L;
if (!timestamptz) {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
}
ts.millis = millis;
return ts;
}
private ParsedBinaryTimestamp toParsedTimestampAbsTimeBin(TimeZone tz, byte[] bytes, boolean timestamptz)
throws RedshiftException {
ParsedBinaryTimestamp ts = toParsedTimestampBinAbsTimePlain(bytes);
if (ts.infinity != null) {
return ts;
}
long secs = ts.millis / 1000L;
// secs = toJavaSecs(secs);
long millis = secs * 1000L;
if (!timestamptz) {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
}
ts.millis = millis;
return ts;
}
private ParsedBinaryTimestamp toProlepticParsedTimestampBin(byte[] bytes)
throws RedshiftException {
ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes);
if (ts.infinity != null) {
return ts;
}
long secs = ts.millis / 1000L;
// postgres epoc to java epoc
secs += 946684800L;
long millis = secs * 1000L;
ts.millis = millis;
return ts;
}
//JCP! if mvn.project.property.redshift.jdbc.spec >= "JDBC4.2"
/**
* Returns the local date time object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
* @param bytes The binary encoded local date time value.
*
* @return The parsed local date time object.
* @throws RedshiftException If binary format could not be parsed.
*/
public LocalDateTime toLocalDateTimeBin(byte[] bytes) throws RedshiftException {
ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return LocalDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return LocalDateTime.MIN;
}
// hardcode utc because the backend does not provide us the timezone
// Postgres is always UTC
return LocalDateTime.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos, ZoneOffset.UTC);
}
//JCP! endif
/**
* Given a UTC timestamp {@code millis} finds another point in time that is rendered in given time
* zone {@code tz} exactly as "millis in UTC".
*
* For instance, given 7 Jan 16:00 UTC and tz=GMT+02:00 it returns 7 Jan 14:00 UTC == 7 Jan 16:00
* GMT+02:00 Note that is not trivial for timestamps near DST change. For such cases, we rely on
* {@link Calendar} to figure out the proper timestamp.
*
* @param millis source timestamp
* @param tz desired time zone
* @return timestamp that would be rendered in {@code tz} like {@code millis} in UTC
*/
private long guessTimestamp(long millis, TimeZone tz) {
if (tz == null) {
// If client did not provide us with time zone, we use system default time zone
tz = getDefaultTz();
}
// The story here:
// Backend provided us with something like '2015-10-04 13:40' and it did NOT provide us with a
// time zone.
// On top of that, user asked us to treat the timestamp as if it were in GMT+02:00.
//
// The code below creates such a timestamp that is rendered as '2015-10-04 13:40 GMT+02:00'
// In other words, its UTC value should be 11:40 UTC == 13:40 GMT+02:00.
// It is not sufficient to just subtract offset as you might cross DST change as you subtract.
//
// For instance, on 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00
// Suppose we deal with 2000-03-26 02:00:01
// If you subtract offset from the timestamp, the time will be "a hour behind" since
// "just a couple of hours ago the OFFSET was different"
//
// To make a long story short: we have UTC timestamp that looks like "2000-03-26 02:00:01" when
// rendered in UTC tz.
// We want to know another timestamp that will look like "2000-03-26 02:00:01" in Europe/Moscow
// time zone.
if (isSimpleTimeZone(tz.getID())) {
// For well-known non-DST time zones, just subtract offset
return millis - tz.getRawOffset();
}
// For all the other time zones, enjoy debugging Calendar API
// Here we do a straight-forward implementation that splits original timestamp into pieces and
// composes it back.
// Note: cal.setTimeZone alone is not sufficient as it would alter hour (it will try to keep the
// same time instant value)
Calendar cal = calendarWithUserTz;
cal.setTimeZone(utcTz);
cal.setTimeInMillis(millis);
int era = cal.get(Calendar.ERA);
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int day = cal.get(Calendar.DAY_OF_MONTH);
int hour = cal.get(Calendar.HOUR_OF_DAY);
int min = cal.get(Calendar.MINUTE);
int sec = cal.get(Calendar.SECOND);
int ms = cal.get(Calendar.MILLISECOND);
cal.setTimeZone(tz);
cal.set(Calendar.ERA, era);
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month);
cal.set(Calendar.DAY_OF_MONTH, day);
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, min);
cal.set(Calendar.SECOND, sec);
cal.set(Calendar.MILLISECOND, ms);
return cal.getTimeInMillis();
}
private static boolean isSimpleTimeZone(String id) {
return id.startsWith("GMT") || id.startsWith("UTC");
}
/**
* Extracts the date part from a timestamp.
*
* @param millis The timestamp from which to extract the date.
* @param tz The time zone of the date.
* @return The extracted date.
*/
public Date convertToDate(long millis, TimeZone tz) {
// no adjustments for the inifity hack values
if (millis <= RedshiftStatement.DATE_NEGATIVE_INFINITY
|| millis >= RedshiftStatement.DATE_POSITIVE_INFINITY) {
return new Date(millis);
}
if (tz == null) {
tz = getDefaultTz();
}
if (isSimpleTimeZone(tz.getID())) {
// Truncate to 00:00 of the day.
// Suppose the input date is 7 Jan 15:40 GMT+02:00 (that is 13:40 UTC)
// We want it to become 7 Jan 00:00 GMT+02:00
// 1) Make sure millis becomes 15:40 in UTC, so add offset
int offset = tz.getRawOffset();
millis += offset;
// 2) Truncate hours, minutes, etc. Day is always 86400 seconds, no matter what leap seconds
// are
millis = floorDiv(millis, ONEDAY) * ONEDAY;
// 2) Now millis is 7 Jan 00:00 UTC, however we need that in GMT+02:00, so subtract some
// offset
millis -= offset;
// Now we have brand-new 7 Jan 00:00 GMT+02:00
return new Date(millis);
}
Calendar cal = calendarWithUserTz;
cal.setTimeZone(tz);
cal.setTimeInMillis(millis);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return new Date(cal.getTimeInMillis());
}
/**
* Extracts the time part from a timestamp. This method ensures the date part of output timestamp
* looks like 1970-01-01 in given timezone.
*
* @param millis The timestamp from which to extract the time.
* @param tz timezone to use.
* @return The extracted time.
*/
public Time convertToTime(long millis, TimeZone tz) {
if (tz == null) {
tz = getDefaultTz();
}
if (isSimpleTimeZone(tz.getID())) {
// Leave just time part of the day.
// Suppose the input date is 2015 7 Jan 15:40 GMT+02:00 (that is 13:40 UTC)
// We want it to become 1970 1 Jan 15:40 GMT+02:00
// 1) Make sure millis becomes 15:40 in UTC, so add offset
int offset = tz.getRawOffset();
millis += offset;
// 2) Truncate year, month, day. Day is always 86400 seconds, no matter what leap seconds are
millis = floorMod(millis, ONEDAY);
// 2) Now millis is 1970 1 Jan 15:40 UTC, however we need that in GMT+02:00, so subtract some
// offset
millis -= offset;
// Now we have brand-new 1970 1 Jan 15:40 GMT+02:00
return new Time(millis);
}
Calendar cal = calendarWithUserTz;
cal.setTimeZone(tz);
cal.setTimeInMillis(millis);
cal.set(Calendar.ERA, GregorianCalendar.AD);
cal.set(Calendar.YEAR, 1970);
cal.set(Calendar.MONTH, 0);
cal.set(Calendar.DAY_OF_MONTH, 1);
return new Time(cal.getTimeInMillis());
}
/**
* Returns the given time value as String matching what the current Redshift server would send
* in text mode.
*
* @param time time value
* @param withTimeZone whether timezone should be added
* @return given time value as String
*/
public String timeToString(java.util.Date time, boolean withTimeZone) {
Calendar cal = null;
if (withTimeZone) {
cal = calendarWithUserTz;
cal.setTimeZone(timeZoneProvider.get());
}
if (time instanceof Timestamp) {
return toString(cal, (Timestamp) time, withTimeZone);
}
if (time instanceof Time) {
return toString(cal, (Time) time, withTimeZone);
}
return toString(cal, (Date) time, withTimeZone);
}
/**
* Converts the given Redshift seconds to java seconds. Reverse engineered by inserting varying
* dates to Redshift and tuning the formula until the java dates matched. See {@link #toPgSecs}
* for the reverse operation.
*
* @param secs Redshift seconds.
* @return Java seconds.
*/
private static long toJavaSecs(long secs) {
// postgres epoc to java epoc
secs += 946684800L;
// Julian/Gregorian calendar cutoff point
if (secs < -12219292800L) { // October 4, 1582 -> October 15, 1582
secs += 86400 * 10;
if (secs < -14825808000L) { // 1500-02-28 -> 1500-03-01
int extraLeaps = (int) ((secs + 14825808000L) / 3155760000L);
extraLeaps--;
extraLeaps -= extraLeaps / 4;
secs += extraLeaps * 86400L;
}
}
return secs;
}
/**
* Converts the given java seconds to Redshift seconds. See {@link #toJavaSecs} for the reverse
* operation. The conversion is valid for any year 100 BC onwards.
*
* @param secs Redshift seconds.
* @return Java seconds.
*/
private static long toPgSecs(long secs) {
// java epoc to postgres epoc
secs -= 946684800L;
// Julian/Greagorian calendar cutoff point
if (secs < -13165977600L) { // October 15, 1582 -> October 4, 1582
secs -= 86400 * 10;
if (secs < -15773356800L) { // 1500-03-01 -> 1500-02-28
int years = (int) ((secs + 15773356800L) / -3155823050L);
years++;
years -= years / 4;
secs += years * 86400L;
}
}
return secs;
}
/**
* Converts the SQL Date to binary representation for {@link Oid#DATE}.
*
* @param tz The timezone used.
* @param bytes The binary encoded date value.
* @param value value
* @throws RedshiftException If binary format could not be parsed.
*/
public void toBinDate(TimeZone tz, byte[] bytes, Date value) throws RedshiftException {
long millis = value.getTime();
if (tz == null) {
tz = getDefaultTz();
}
// It "getOffset" is UNTESTED
// See com.amazon.redshift.jdbc.AbstractJdbc2Statement.setDate(int, java.sql.Date,
// java.util.Calendar)
// The problem is we typically do not know for sure what is the exact required date/timestamp
// type
// Thus pgjdbc sticks to text transfer.
millis += tz.getOffset(millis);
long secs = toPgSecs(millis / 1000);
ByteConverter.int4(bytes, 0, (int) (secs / 86400));
}
/**
* Converts the SQL Timestamp to binary representation for {@link Oid#TIMESTAMP}.
*
* @param tz The timezone used.
* @param bytes The binary encoded Timestamp value.
* @param value value
* @throws RedshiftException If binary format could not be parsed.
*/
public void toBinTimestamp(TimeZone tz, byte[] bytes, Timestamp value) throws RedshiftException {
long millis = value.getTime();
if (tz == null) {
tz = getDefaultTz();
}
// It "getOffset" is UNTESTED
// See com.amazon.redshift.jdbc.AbstractJdbc2Statement.setDate(int, java.sql.Date,
// java.util.Calendar)
// The problem is we typically do not know for sure what is the exact required date/timestamp
// type
// Thus pgjdbc sticks to text transfer.
// millis += tz.getOffset(millis);
long secs = toPgSecs(millis / 1000);
ByteConverter.int8(bytes, 0, (long) (secs * 1000000));
}
/**
* Converts backend's TimeZone parameter to java format.
* Notable difference: backend's gmt-3 is GMT+03 in Java.
*
* @param timeZone time zone to use
* @return java TimeZone
*/
public static TimeZone parseBackendTimeZone(String timeZone) {
if (timeZone.startsWith("GMT")) {
TimeZone tz = GMT_ZONES.get(timeZone);
if (tz != null) {
return tz;
}
}
return TimeZone.getTimeZone(timeZone);
}
private static long floorDiv(long x, long y) {
long r = x / y;
// if the signs are different and modulo not zero, round down
if ((x ^ y) < 0 && (r * y != x)) {
r--;
}
return r;
}
private static long floorMod(long x, long y) {
return x - floorDiv(x, y) * y;
}
}