From 9bf8c8b23d2ccf7324430a1c38c4ccf6343ade09 Mon Sep 17 00:00:00 2001 From: Benjamin Zikarsky Date: Thu, 21 Nov 2019 11:42:20 +0100 Subject: [PATCH 1/3] Properly map java.time.Instant to TIMESTAMPTZ Java's `Instant` type is a fixed point in time (unix epoch which is always UTC) and therefore similar to `OffsetDateTime`. Currently Instant is mapped to a `TIMESTAMP` which is incorrect. This commit changes this to `TIMESTAMPTZ` which is Postgres' type for specific points in time and is the same way the official driver handles this. --- .../java/io/r2dbc/postgresql/codec/InstantCodec.java | 12 ++++-------- .../postgresql/AbstractCodecIntegrationTests.java | 2 +- .../io/r2dbc/postgresql/codec/InstantCodecTest.java | 6 ++++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/r2dbc/postgresql/codec/InstantCodec.java b/src/main/java/io/r2dbc/postgresql/codec/InstantCodec.java index 7812d098..8819cf33 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/InstantCodec.java +++ b/src/main/java/io/r2dbc/postgresql/codec/InstantCodec.java @@ -25,14 +25,10 @@ import io.r2dbc.postgresql.util.ByteBufUtils; import reactor.util.annotation.Nullable; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; +import java.time.*; import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT; -import static io.r2dbc.postgresql.type.PostgresqlObjectId.TIMESTAMP; +import static io.r2dbc.postgresql.type.PostgresqlObjectId.TIMESTAMPTZ; final class InstantCodec extends AbstractTemporalCodec { @@ -45,7 +41,7 @@ final class InstantCodec extends AbstractTemporalCodec { @Override public Parameter encodeNull() { - return createNull(TIMESTAMP, FORMAT_TEXT); + return createNull(TIMESTAMPTZ, FORMAT_TEXT); } @Override @@ -70,7 +66,7 @@ Instant doDecode(ByteBuf buffer, PostgresqlObjectId dataType, @Nullable Format f Parameter doEncode(Instant value) { Assert.requireNonNull(value, "value must not be null"); - return create(TIMESTAMP, FORMAT_TEXT, () -> ByteBufUtils.encode(this.byteBufAllocator, value.toString())); + return create(TIMESTAMPTZ, FORMAT_TEXT, () -> ByteBufUtils.encode(this.byteBufAllocator, value.toString())); } @Override diff --git a/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java b/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java index f411153c..3c5d677f 100644 --- a/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java +++ b/src/test/java/io/r2dbc/postgresql/AbstractCodecIntegrationTests.java @@ -180,7 +180,7 @@ void inetAddress() throws UnknownHostException { @Test void instant() { - testCodec(Instant.class, Instant.now(), "TIMESTAMP"); + testCodec(Instant.class, Instant.now(), "TIMESTAMPTZ"); } @Test diff --git a/src/test/java/io/r2dbc/postgresql/codec/InstantCodecTest.java b/src/test/java/io/r2dbc/postgresql/codec/InstantCodecTest.java index 62c2b913..ca1dae03 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/InstantCodecTest.java +++ b/src/test/java/io/r2dbc/postgresql/codec/InstantCodecTest.java @@ -75,6 +75,8 @@ void doCanDecode() { assertThat(codec.doCanDecode(TIMESTAMP, FORMAT_BINARY)).isTrue(); assertThat(codec.doCanDecode(MONEY, FORMAT_TEXT)).isFalse(); assertThat(codec.doCanDecode(TIMESTAMP, FORMAT_TEXT)).isTrue(); + assertThat(codec.doCanDecode(TIMESTAMPTZ, FORMAT_TEXT)).isTrue(); + assertThat(codec.doCanDecode(TIMESTAMPTZ, FORMAT_BINARY)).isTrue(); } @Test @@ -95,7 +97,7 @@ void doEncode() { assertThat(new InstantCodec(TEST).doEncode(instant)) .hasFormat(FORMAT_TEXT) - .hasType(TIMESTAMP.getObjectId()) + .hasType(TIMESTAMPTZ.getObjectId()) .hasValue(encode(TEST, instant.toString())); } @@ -108,7 +110,7 @@ void doEncodeNoValue() { @Test void encodeNull() { assertThat(new InstantCodec(TEST).encodeNull()) - .isEqualTo(new Parameter(FORMAT_TEXT, TIMESTAMP.getObjectId(), NULL_VALUE)); + .isEqualTo(new Parameter(FORMAT_TEXT, TIMESTAMPTZ.getObjectId(), NULL_VALUE)); } } From 1d074defa193e82f95d437ba9ba5f71220b67263 Mon Sep 17 00:00:00 2001 From: Benjamin Zikarsky Date: Thu, 21 Nov 2019 12:21:08 +0100 Subject: [PATCH 2/3] Use LocaleDateTime codec for legacy Date-API The current DateCodec for legacy `java.util.Date` was using `java.time.Instant` which was itself incorrectly mapping to `TIMESTAMP`. With the previous fix to `InstantCodec` the codec of `Date` had to be changed so the more correct `LocalDateTimeCodec`. --- .../java/io/r2dbc/postgresql/codec/DateCodec.java | 12 +++++++----- .../r2dbc/postgresql/codec/LocalDateTimeCodec.java | 8 +++++--- .../io/r2dbc/postgresql/codec/DateCodecTest.java | 10 ++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/r2dbc/postgresql/codec/DateCodec.java b/src/main/java/io/r2dbc/postgresql/codec/DateCodec.java index 02bc91d0..abcbdcd9 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/DateCodec.java +++ b/src/main/java/io/r2dbc/postgresql/codec/DateCodec.java @@ -24,18 +24,19 @@ import io.r2dbc.postgresql.util.Assert; import reactor.util.annotation.Nullable; -import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Date; final class DateCodec extends AbstractCodec { - private final InstantCodec delegate; + private final LocalDateTimeCodec delegate; DateCodec(ByteBufAllocator byteBufAllocator) { super(Date.class); Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - this.delegate = new InstantCodec(byteBufAllocator); + this.delegate = new LocalDateTimeCodec(byteBufAllocator); } @Override @@ -55,14 +56,15 @@ boolean doCanDecode(PostgresqlObjectId type, Format format) { Date doDecode(ByteBuf buffer, PostgresqlObjectId dataType, @Nullable Format format, @Nullable Class type) { Assert.requireNonNull(buffer, "byteBuf must not be null"); - return Date.from(this.delegate.doDecode(buffer, dataType, format, Instant.class)); + LocalDateTime intermediary = this.delegate.doDecode(buffer, dataType, format, LocalDateTime.class); + return Date.from(intermediary.atZone(ZoneId.systemDefault()).toInstant()); } @Override Parameter doEncode(Date value) { Assert.requireNonNull(value, "value must not be null"); - return this.delegate.doEncode(value.toInstant()); + return this.delegate.doEncode(value.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()); } } diff --git a/src/main/java/io/r2dbc/postgresql/codec/LocalDateTimeCodec.java b/src/main/java/io/r2dbc/postgresql/codec/LocalDateTimeCodec.java index fcaff4ba..40170f2e 100644 --- a/src/main/java/io/r2dbc/postgresql/codec/LocalDateTimeCodec.java +++ b/src/main/java/io/r2dbc/postgresql/codec/LocalDateTimeCodec.java @@ -25,9 +25,7 @@ import io.r2dbc.postgresql.util.ByteBufUtils; import reactor.util.annotation.Nullable; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.*; import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT; import static io.r2dbc.postgresql.type.PostgresqlObjectId.TIMESTAMP; @@ -51,6 +49,10 @@ LocalDateTime doDecode(ByteBuf buffer, PostgresqlObjectId dataType, @Nullable Fo Assert.requireNonNull(buffer, "byteBuf must not be null"); return decodeTemporal(buffer, dataType, format, LocalDateTime.class, temporal -> { + if (temporal instanceof LocalDate) { + return ((LocalDate) temporal).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime(); + } + return Instant.from(temporal).atOffset(ZoneOffset.UTC).toLocalDateTime(); }); } diff --git a/src/test/java/io/r2dbc/postgresql/codec/DateCodecTest.java b/src/test/java/io/r2dbc/postgresql/codec/DateCodecTest.java index 00b11d3b..4160de9a 100644 --- a/src/test/java/io/r2dbc/postgresql/codec/DateCodecTest.java +++ b/src/test/java/io/r2dbc/postgresql/codec/DateCodecTest.java @@ -49,9 +49,10 @@ void constructorNoByteBufAllocator() { @Test void decode() { - Date date = Date.from(Instant.parse("2018-11-04T15:37:31.177Z")); + Instant testInstant = LocalDateTime.parse("2010-02-01T10:08:04.412").atZone(ZoneId.systemDefault()).toInstant(); + Date date = Date.from(testInstant); - assertThat(new DateCodec(TEST).decode(encode(TEST, "2018-11-04 15:37:31.177"), dataType, FORMAT_TEXT, Date.class)) + assertThat(new DateCodec(TEST).decode(encode(TEST, "2010-02-01 10:08:04.412"), dataType, FORMAT_TEXT, Date.class)) .isEqualTo(date); } @@ -91,12 +92,13 @@ void doCanDecodeNoType() { @Test void doEncode() { - Date date = new Date(); + Instant testInstant = LocalDateTime.parse("2010-02-01T10:08:04.412").atZone(ZoneId.systemDefault()).toInstant(); + Date date = Date.from(testInstant); assertThat(new DateCodec(TEST).doEncode(date)) .hasFormat(FORMAT_TEXT) .hasType(TIMESTAMP.getObjectId()) - .hasValue(encode(TEST, date.toInstant().toString())); + .hasValue(encode(TEST, "2010-02-01T10:08:04.412")); } @Test From 55e641d19af5b88c75c3e7c94a57b089f6005d46 Mon Sep 17 00:00:00 2001 From: Benjamin Zikarsky Date: Thu, 21 Nov 2019 14:05:13 +0100 Subject: [PATCH 3/3] Add codecs for `Instant` and legacy `Date` to README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb4574a2..eef1eed3 100644 --- a/README.md +++ b/README.md @@ -278,8 +278,8 @@ This reference table shows the type mapping between [PostgreSQL][p] and Java dat | [`text`][psql-text-ref] | [**`String`**][java-string-ref], `Clob`| | [`time [without time zone]`][psql-time-ref] | [`LocalTime`][java-lt-ref]| | [`time [with time zone]`][psql-time-ref] | Not yet supported.| -| [`timestamp [without time zone]`][psql-time-ref]|[**`LocalDateTime`**][java-ldt-ref], [`LocalTime`][java-lt-ref], [`LocalDate`][java-ld-ref]| -| [`timestamp [with time zone]`][psql-time-ref] | [**`OffsetDatetime`**][java-odt-ref], [`ZonedDateTime`][java-zdt-ref]| +| [`timestamp [without time zone]`][psql-time-ref]|[**`LocalDateTime`**][java-ldt-ref], [`LocalTime`][java-lt-ref], [`LocalDate`][java-ld-ref], [`java.util.Date`][java-legacy-date-ref]| +| [`timestamp [with time zone]`][psql-time-ref] | [**`OffsetDatetime`**][java-odt-ref], [`ZonedDateTime`][java-zdt-ref], [`Instant`][java-instant-ref]| | [`tsquery`][psql-tsquery-ref] | Not yet supported.| | [`tsvector`][psql-tsvector-ref] | Not yet supported.| | [`txid_snapshot`][psql-txid_snapshot-ref] | Not yet supported.| @@ -349,6 +349,7 @@ Support for the following single-dimensional arrays (read and write): [java-ldt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html [java-ld-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html [java-lt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html +[java-legacy-date-ref]: https://docs.oracle.com/javase/8/docs/api/java/util/Date.html [java-odt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html [java-primitive-ref]: https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html [java-short-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Short.html