提交 e0af493c authored 作者: Noel Grandin's avatar Noel Grandin

fix timezone conversion in 'TIMESTAMP WITH TIMEZONE' datatype

上级 5a038869
...@@ -5,31 +5,65 @@ ...@@ -5,31 +5,65 @@
*/ */
package org.h2.api; package org.h2.api;
import java.sql.Timestamp; import java.io.Serializable;
import org.h2.util.DateTimeUtils;
import org.h2.util.StringUtils;
/** /**
* Extends java.sql.Timestamp to add our time zone information. * How we expose "DATETIME WITH TIMEZONE" in our ResultSets.
*/ */
public class TimestampWithTimeZone extends Timestamp { public class TimestampWithTimeZone implements Serializable, Cloneable {
/** /**
* The serial version UID. * The serial version UID.
*/ */
private static final long serialVersionUID = 4413229090646777107L; private static final long serialVersionUID = 4413229090646777107L;
/**
* A bit field with bits for the year, month, and day (see DateTimeUtils for
* encoding)
*/
private final long dateValue;
/**
* The nanoseconds since midnight.
*/
private final long timeNanos;
/** /**
* Time zone offset from UTC in minutes, range of -12hours to +12hours * Time zone offset from UTC in minutes, range of -12hours to +12hours
*/ */
private final short timeZoneOffsetMins; private final short timeZoneOffsetMins;
public TimestampWithTimeZone(long timeMillis, int nanos, short timeZoneOffsetMins) { public TimestampWithTimeZone(long dateValue, long timeNanos, short timeZoneOffsetMins) {
super(timeMillis); this.dateValue = dateValue;
setNanos(nanos); this.timeNanos = timeNanos;
this.timeZoneOffsetMins = timeZoneOffsetMins; this.timeZoneOffsetMins = timeZoneOffsetMins;
} }
/** /**
* The timezone offset in minutes. * @return the year-month-day bit field
*/
public long getYMD() {
return dateValue;
}
public long getYear() {
return DateTimeUtils.yearFromDateValue(dateValue);
}
public long getMonth() {
return DateTimeUtils.monthFromDateValue(dateValue);
}
public long getDay() {
return DateTimeUtils.dayFromDateValue(dateValue);
}
public long getNanosSinceMidnight() {
return timeNanos;
}
/**
* The time zone offset in minutes.
* *
* @return the offset * @return the offset
*/ */
...@@ -37,6 +71,66 @@ public class TimestampWithTimeZone extends Timestamp { ...@@ -37,6 +71,66 @@ public class TimestampWithTimeZone extends Timestamp {
return timeZoneOffsetMins; return timeZoneOffsetMins;
} }
@Override
public String toString() {
StringBuilder buff = new StringBuilder();
int y = DateTimeUtils.yearFromDateValue(dateValue);
int month = DateTimeUtils.monthFromDateValue(dateValue);
int d = DateTimeUtils.dayFromDateValue(dateValue);
if (y > 0 && y < 10000) {
StringUtils.appendZeroPadded(buff, 4, y);
} else {
buff.append(y);
}
buff.append('-');
StringUtils.appendZeroPadded(buff, 2, month);
buff.append('-');
StringUtils.appendZeroPadded(buff, 2, d);
buff.append(' ');
long nanos = timeNanos;
long ms = nanos / 1000000;
nanos -= ms * 1000000;
long s = ms / 1000;
ms -= s * 1000;
long min = s / 60;
s -= min * 60;
long h = min / 60;
min -= h * 60;
StringUtils.appendZeroPadded(buff, 2, h);
buff.append(':');
StringUtils.appendZeroPadded(buff, 2, min);
buff.append(':');
StringUtils.appendZeroPadded(buff, 2, s);
buff.append('.');
int start = buff.length();
StringUtils.appendZeroPadded(buff, 3, ms);
if (nanos > 0) {
StringUtils.appendZeroPadded(buff, 6, nanos);
}
for (int i = buff.length() - 1; i > start; i--) {
if (buff.charAt(i) != '0') {
break;
}
buff.deleteCharAt(i);
}
short tz = timeZoneOffsetMins;
if (tz < 0) {
buff.append('-');
tz = (short) -tz;
} else {
buff.append('+');
}
int hours = tz / 60;
tz -= hours * 60;
int mins = tz;
StringUtils.appendZeroPadded(buff, 2, hours);
if (mins != 0) {
buff.append(':');
StringUtils.appendZeroPadded(buff, 2, mins);
}
return buff.toString();
}
@Override @Override
public int hashCode() { public int hashCode() {
return 31 * super.hashCode() + timeZoneOffsetMins; return 31 * super.hashCode() + timeZoneOffsetMins;
...@@ -47,13 +141,16 @@ public class TimestampWithTimeZone extends Timestamp { ...@@ -47,13 +141,16 @@ public class TimestampWithTimeZone extends Timestamp {
if (this == obj) { if (this == obj) {
return true; return true;
} }
if (!super.equals(obj)) {
return false;
}
if (getClass() != obj.getClass()) { if (getClass() != obj.getClass()) {
return false; return false;
} }
TimestampWithTimeZone other = (TimestampWithTimeZone) obj; TimestampWithTimeZone other = (TimestampWithTimeZone) obj;
if (dateValue != other.dateValue) {
return false;
}
if (timeNanos != other.timeNanos) {
return false;
}
if (timeZoneOffsetMins != other.timeZoneOffsetMins) { if (timeZoneOffsetMins != other.timeZoneOffsetMins) {
return false; return false;
} }
......
...@@ -20,6 +20,7 @@ import org.h2.message.DbException; ...@@ -20,6 +20,7 @@ import org.h2.message.DbException;
import org.h2.result.Row; import org.h2.result.Row;
import org.h2.schema.Schema; import org.h2.schema.Schema;
import org.h2.schema.Sequence; import org.h2.schema.Sequence;
import org.h2.util.DateTimeUtils;
import org.h2.util.MathUtils; import org.h2.util.MathUtils;
import org.h2.util.StringUtils; import org.h2.util.StringUtils;
import org.h2.value.DataType; import org.h2.value.DataType;
...@@ -298,8 +299,10 @@ public class Column { ...@@ -298,8 +299,10 @@ public class Column {
} else if (dt.type == Value.TIMESTAMP_UTC) { } else if (dt.type == Value.TIMESTAMP_UTC) {
value = ValueTimestampUtc.fromMillis(session.getTransactionStart()); value = ValueTimestampUtc.fromMillis(session.getTransactionStart());
} else if (dt.type == Value.TIMESTAMP_TZ) { } else if (dt.type == Value.TIMESTAMP_TZ) {
value = ValueTimestampTimeZone.fromMillis( long ms = session.getTransactionStart();
session.getTransactionStart(), (short) 0); value = ValueTimestampTimeZone.fromDateValueAndNanos(
DateTimeUtils.dateValueFromDate(ms),
DateTimeUtils.nanosFromDate(ms), (short) 0);
} else if (dt.type == Value.TIME) { } else if (dt.type == Value.TIME) {
value = ValueTime.fromNanos(0); value = ValueTime.fromNanos(0);
} else if (dt.type == Value.DATE) { } else if (dt.type == Value.DATE) {
......
...@@ -15,7 +15,6 @@ import java.util.Calendar; ...@@ -15,7 +15,6 @@ import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.message.DbException; import org.h2.message.DbException;
import org.h2.value.Value; import org.h2.value.Value;
...@@ -683,6 +682,18 @@ public class DateTimeUtils { ...@@ -683,6 +682,18 @@ public class DateTimeUtils {
return new Date(millis); return new Date(millis);
} }
/**
* Convert a date value to millis, using the supplied timezone.
*
* @param dateValue the date value
* @return the date
*/
public static long convertDateValueToMillis(TimeZone tz, long dateValue) {
return getMillis(tz,
yearFromDateValue(dateValue),
monthFromDateValue(dateValue),
dayFromDateValue(dateValue), 0, 0, 0, 0);
}
/** /**
* Convert a date value / time value to a timestamp, using the default * Convert a date value / time value to a timestamp, using the default
* timezone. * timezone.
......
...@@ -561,7 +561,7 @@ public class DataType { ...@@ -561,7 +561,7 @@ public class DataType {
break; break;
} }
case Value.TIMESTAMP_TZ: { case Value.TIMESTAMP_TZ: {
TimestampWithTimeZone value = (TimestampWithTimeZone) rs.getTimestamp(columnIndex); TimestampWithTimeZone value = (TimestampWithTimeZone) rs.getObject(columnIndex);
v = value == null ? (Value) ValueNull.INSTANCE : v = value == null ? (Value) ValueNull.INSTANCE :
ValueTimestampTimeZone.get(value); ValueTimestampTimeZone.get(value);
break; break;
......
/* /*
* Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, and the
* and the EPL 1.0 (http://h2database.com/html/license.html). * EPL 1.0 (http://h2database.com/html/license.html). Initial Developer: H2
* Initial Developer: H2 Group * Group
*/ */
package org.h2.value; package org.h2.value;
...@@ -25,14 +25,16 @@ import org.h2.util.StringUtils; ...@@ -25,14 +25,16 @@ import org.h2.util.StringUtils;
*/ */
public class ValueTimestampTimeZone extends Value { public class ValueTimestampTimeZone extends Value {
private static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
/** /**
* The precision in digits. * The precision in digits.
*/ */
public static final int PRECISION = 30; public static final int PRECISION = 30;
/** /**
* The display size of the textual representation of a timestamp. * The display size of the textual representation of a timestamp. Example:
* Example: 2001-01-01 23:59:59.000 +10:00 * 2001-01-01 23:59:59.000 +10:00
*/ */
static final int DISPLAY_SIZE = 30; static final int DISPLAY_SIZE = 30;
...@@ -58,10 +60,11 @@ public class ValueTimestampTimeZone extends Value { ...@@ -58,10 +60,11 @@ public class ValueTimestampTimeZone extends Value {
private ValueTimestampTimeZone(long dateValue, long timeNanos, private ValueTimestampTimeZone(long dateValue, long timeNanos,
short timeZoneOffsetMins) { short timeZoneOffsetMins) {
if (timeNanos < 0 || timeNanos >= 24L * 60 * 60 * 1000 * 1000 * 1000) { if (timeNanos < 0 || timeNanos >= 24L * 60 * 60 * 1000 * 1000 * 1000) {
throw new IllegalArgumentException("timeNanos out of range " + throw new IllegalArgumentException(
timeNanos); "timeNanos out of range " + timeNanos);
} }
if (timeZoneOffsetMins < (-12 * 60) || timeZoneOffsetMins >= (12 * 60)) { if (timeZoneOffsetMins < (-12 * 60)
|| timeZoneOffsetMins >= (12 * 60)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"timeZoneOffsetMins out of range " + timeZoneOffsetMins); "timeZoneOffsetMins out of range " + timeZoneOffsetMins);
} }
...@@ -92,43 +95,11 @@ public class ValueTimestampTimeZone extends Value { ...@@ -92,43 +95,11 @@ public class ValueTimestampTimeZone extends Value {
* @return the value * @return the value
*/ */
public static ValueTimestampTimeZone get(TimestampWithTimeZone timestamp) { public static ValueTimestampTimeZone get(TimestampWithTimeZone timestamp) {
long ms = timestamp.getTime(); return fromDateValueAndNanos(timestamp.getYMD(),
long nanos = timestamp.getNanos() % 1000000; timestamp.getNanosSinceMidnight(),
long dateValue = DateTimeUtils.dateValueFromDate(ms);
nanos += DateTimeUtils.nanosFromDate(ms);
return fromDateValueAndNanos(dateValue, nanos,
timestamp.getTimeZoneOffsetMins()); timestamp.getTimeZoneOffsetMins());
} }
/**
* Get or create a timestamp value for the given date/time in millis.
*
* @param ms the milliseconds
* @param nanos the nanoseconds
* @param timeZoneOffsetMins the timezone offset in minutes
* @return the value
*/
public static ValueTimestampTimeZone fromMillisNanos(long ms, int nanos,
short timeZoneOffsetMins) {
long dateValue = DateTimeUtils.dateValueFromDate(ms);
long timeNanos = nanos + DateTimeUtils.nanosFromDate(ms);
return fromDateValueAndNanos(dateValue, timeNanos, timeZoneOffsetMins);
}
/**
* Get or create a timestamp value for the given date/time in millis.
*
* @param ms the milliseconds
* @param timeZoneOffsetMins the timezone offset in minutes
* @return the value
*/
public static ValueTimestampTimeZone fromMillis(long ms,
short timeZoneOffsetMins) {
long dateValue = DateTimeUtils.dateValueFromDate(ms);
long nanos = DateTimeUtils.nanosFromDate(ms);
return fromDateValueAndNanos(dateValue, nanos, timeZoneOffsetMins);
}
/** /**
* Parse a string to a ValueTimestamp. This method supports the format * Parse a string to a ValueTimestamp. This method supports the format
* +/-year-month-day hour:minute:seconds.fractional and an optional timezone * +/-year-month-day hour:minute:seconds.fractional and an optional timezone
...@@ -141,8 +112,8 @@ public class ValueTimestampTimeZone extends Value { ...@@ -141,8 +112,8 @@ public class ValueTimestampTimeZone extends Value {
try { try {
return parseTry(s); return parseTry(s);
} catch (Exception e) { } catch (Exception e) {
throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, e,
e, "TIMESTAMP WITH TIMEZONE", s); "TIMESTAMP WITH TIMEZONE", s);
} }
} }
...@@ -194,7 +165,8 @@ public class ValueTimestampTimeZone extends Value { ...@@ -194,7 +165,8 @@ public class ValueTimestampTimeZone extends Value {
} }
} }
if (tz != null) { if (tz != null) {
long millis = DateTimeUtils.convertDateValueToDate(dateValue).getTime(); long millis = DateTimeUtils
.convertDateValueToMillis(GMT_TIMEZONE, dateValue);
tzMinutes = (short) (tz.getOffset(millis) / 1000 / 60); tzMinutes = (short) (tz.getOffset(millis) / 1000 / 60);
} }
} }
...@@ -234,10 +206,7 @@ public class ValueTimestampTimeZone extends Value { ...@@ -234,10 +206,7 @@ public class ValueTimestampTimeZone extends Value {
@Override @Override
public Timestamp getTimestamp() { public Timestamp getTimestamp() {
Timestamp ts = DateTimeUtils.convertDateValueToTimestamp(dateValue, throw new UnsupportedOperationException("unimplemented");
timeNanos);
return new TimestampWithTimeZone(ts.getTime(), ts.getNanos(),
getTimeZoneOffsetMins());
} }
@Override @Override
...@@ -340,19 +309,20 @@ public class ValueTimestampTimeZone extends Value { ...@@ -340,19 +309,20 @@ public class ValueTimestampTimeZone extends Value {
return false; return false;
} }
ValueTimestampTimeZone x = (ValueTimestampTimeZone) other; ValueTimestampTimeZone x = (ValueTimestampTimeZone) other;
return dateValue == x.dateValue && timeNanos == x.timeNanos && return dateValue == x.dateValue && timeNanos == x.timeNanos
timeZoneOffsetMins == x.timeZoneOffsetMins; && timeZoneOffsetMins == x.timeZoneOffsetMins;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return (int) (dateValue ^ (dateValue >>> 32) ^ timeNanos ^ return (int) (dateValue ^ (dateValue >>> 32) ^ timeNanos
(timeNanos >>> 32) ^ timeZoneOffsetMins); ^ (timeNanos >>> 32) ^ timeZoneOffsetMins);
} }
@Override @Override
public Object getObject() { public Object getObject() {
return getTimestamp(); return new TimestampWithTimeZone(dateValue, timeNanos,
timeZoneOffsetMins);
} }
@Override @Override
...@@ -363,15 +333,13 @@ public class ValueTimestampTimeZone extends Value { ...@@ -363,15 +333,13 @@ public class ValueTimestampTimeZone extends Value {
@Override @Override
public Value add(Value v) { public Value add(Value v) {
throw DbException throw DbException.getUnsupportedException(
.getUnsupportedException(
"manipulating TIMESTAMP WITH TIMEZONE values is unsupported"); "manipulating TIMESTAMP WITH TIMEZONE values is unsupported");
} }
@Override @Override
public Value subtract(Value v) { public Value subtract(Value v) {
throw DbException throw DbException.getUnsupportedException(
.getUnsupportedException(
"manipulating TIMESTAMP WITH TIMEZONE values is unsupported"); "manipulating TIMESTAMP WITH TIMEZONE values is unsupported");
} }
......
...@@ -679,6 +679,23 @@ public abstract class TestBase { ...@@ -679,6 +679,23 @@ public abstract class TestBase {
} }
} }
/**
* Check if two values are equal, and if not throw an exception.
*
* @param expected the expected value
* @param actual the actual value
* @throws AssertionError if the values are not equal
*/
public void assertEquals(Object expected, Object actual) {
if (expected == null || actual == null) {
assertTrue(expected == actual);
return;
}
if (!expected.equals(actual)) {
fail(" expected: " + expected + " actual: " + actual);
}
}
/** /**
* Check if two readers are equal, and if not throw an exception. * Check if two readers are equal, and if not throw an exception.
* *
......
...@@ -5,11 +5,10 @@ ...@@ -5,11 +5,10 @@
*/ */
package org.h2.test.jaqu; package org.h2.test.jaqu;
import static java.sql.Date.valueOf;
import org.h2.jaqu.Db; import org.h2.jaqu.Db;
import org.h2.test.TestBase; import org.h2.test.TestBase;
import static java.sql.Date.valueOf;
/** /**
* Tests the Db.update() function. * Tests the Db.update() function.
* *
...@@ -58,7 +57,7 @@ public class UpdateTest extends TestBase { ...@@ -58,7 +57,7 @@ public class UpdateTest extends TestBase {
Product p2 = new Product(); Product p2 = new Product();
Product pChang2 = db.from(p2).where(p2.productName).is("Chang") Product pChang2 = db.from(p2).where(p2.productName).is("Chang")
.selectFirst(); .selectFirst();
assertEquals(19.5, pChang2.unitPrice); assertEquals((Double)19.5, pChang2.unitPrice);
assertEquals(16, pChang2.unitsInStock.intValue()); assertEquals(16, pChang2.unitsInStock.intValue());
// undo update // undo update
...@@ -96,7 +95,7 @@ public class UpdateTest extends TestBase { ...@@ -96,7 +95,7 @@ public class UpdateTest extends TestBase {
Product p2 = new Product(); Product p2 = new Product();
Product pChang2 = db.from(p2).where(p2.productName).is("Chang") Product pChang2 = db.from(p2).where(p2.productName).is("Chang")
.selectFirst(); .selectFirst();
assertEquals(19.5, pChang2.unitPrice); assertEquals((Double)19.5, pChang2.unitPrice);
assertEquals(16, pChang2.unitsInStock.intValue()); assertEquals(16, pChang2.unitsInStock.intValue());
// undo update // undo update
...@@ -137,7 +136,7 @@ public class UpdateTest extends TestBase { ...@@ -137,7 +136,7 @@ public class UpdateTest extends TestBase {
// confirm the data was properly updated // confirm the data was properly updated
Product revised = db.from(p).where(p.productId).is(1).selectFirst(); Product revised = db.from(p).where(p.productId).is(1).selectFirst();
assertEquals("updated", revised.productName); assertEquals("updated", revised.productName);
assertEquals(original.unitPrice + 3.14, revised.unitPrice); assertEquals((Double)(original.unitPrice + 3.14), revised.unitPrice);
assertEquals(original.unitsInStock + 2, revised.unitsInStock.intValue()); assertEquals(original.unitsInStock + 2, revised.unitsInStock.intValue());
// restore the data // restore the data
......
...@@ -15,7 +15,6 @@ import java.util.Map; ...@@ -15,7 +15,6 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.h2.mvstore.DataUtils; import org.h2.mvstore.DataUtils;
import org.h2.mvstore.MVMap; import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore; import org.h2.mvstore.MVStore;
...@@ -68,7 +67,7 @@ public class TestStreamStore extends TestBase { ...@@ -68,7 +67,7 @@ public class TestStreamStore extends TestBase {
if (max == -1) { if (max == -1) {
assertTrue(map.isEmpty()); assertTrue(map.isEmpty());
} else { } else {
assertEquals(map.lastKey(), max); assertEquals(map.lastKey(), (Long)max);
} }
} }
} }
......
...@@ -9,8 +9,7 @@ import java.sql.Connection; ...@@ -9,8 +9,7 @@ import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.sql.Timestamp; import org.h2.api.TimestampWithTimeZone;
import org.h2.test.TestBase; import org.h2.test.TestBase;
/** /**
...@@ -41,11 +40,8 @@ public class TestTimeStampWithTimeZone extends TestBase { ...@@ -41,11 +40,8 @@ public class TestTimeStampWithTimeZone extends TestBase {
ResultSet rs = stat.executeQuery("select t1 from test"); ResultSet rs = stat.executeQuery("select t1 from test");
rs.next(); rs.next();
assertEquals("1970-01-01 12:00:00.0+00:15", rs.getString(1)); assertEquals("1970-01-01 12:00:00.0+00:15", rs.getString(1));
Timestamp ts = rs.getTimestamp(1); TimestampWithTimeZone ts = (TimestampWithTimeZone) rs.getObject(1);
// TODO currently fails: assertEquals(new TimestampWithTimeZone(1008673L, 43200000000000L, (short) 15), ts);
//assertTrue("" + ts,
// new TimestampWithTimeZone(36000000, 00, (short) 15).equals(
// ts));
conn.close(); conn.close();
} }
......
...@@ -167,7 +167,9 @@ public class TestValueMemory extends TestBase implements DataHandler { ...@@ -167,7 +167,9 @@ public class TestValueMemory extends TestBase implements DataHandler {
case Value.TIMESTAMP_UTC: case Value.TIMESTAMP_UTC:
return ValueTimestampUtc.fromMillis(random.nextLong()); return ValueTimestampUtc.fromMillis(random.nextLong());
case Value.TIMESTAMP_TZ: case Value.TIMESTAMP_TZ:
return ValueTimestampTimeZone.fromMillis(random.nextLong(), (short) 0); return ValueTimestampTimeZone.fromDateValueAndNanos(
random.nextLong(), random.nextLong(),
(short) random.nextInt());
case Value.BYTES: case Value.BYTES:
return ValueBytes.get(randomBytes(random.nextInt(1000))); return ValueBytes.get(randomBytes(random.nextInt(1000)));
case Value.STRING: case Value.STRING:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论