Skip to content

Commit 4975494

Browse files
committed
Align LocalDate/LocalDateTime conversion to configured time zone.
Local temporals are converted into their local representation using the session timezone. [closes #521] Signed-off-by: Mark Paluch <[email protected]>
1 parent ba34660 commit 4975494

9 files changed

+143
-61
lines changed

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionFactory.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
import reactor.core.publisher.Mono;
3535
import reactor.util.annotation.Nullable;
3636

37+
import java.time.ZoneId;
3738
import java.util.ArrayList;
3839
import java.util.LinkedHashMap;
3940
import java.util.List;
4041
import java.util.Locale;
4142
import java.util.Map;
43+
import java.util.TimeZone;
4244

4345
/**
4446
* An implementation of {@link ConnectionFactory} for creating connections to a PostgreSQL database.
@@ -115,17 +117,20 @@ public Mono<io.r2dbc.postgresql.api.PostgresqlReplicationConnection> replication
115117

116118
ConnectionSettings connectionSettings = this.configuration.getConnectionSettings().mutate(builder -> builder.startupOptions(options));
117119

118-
ConnectionStrategy connectionStrategy = ConnectionStrategyFactory.getConnectionStrategy(connectionFunction, this.configuration, connectionSettings);
120+
ConnectionStrategy connectionStrategy = ConnectionStrategyFactory.getConnectionStrategy(this.connectionFunction, this.configuration, connectionSettings);
119121

120122
return doCreateConnection(true, connectionStrategy).map(DefaultPostgresqlReplicationConnection::new);
121123
}
122124

123125
private Mono<PostgresqlConnection> doCreateConnection(boolean forReplication, ConnectionStrategy connectionStrategy) {
124126

127+
ZoneId defaultZone = TimeZone.getDefault().toZoneId();
128+
125129
return connectionStrategy.connect()
126130
.flatMap(client -> {
127131

128-
DefaultCodecs codecs = new DefaultCodecs(client.getByteBufAllocator(), this.configuration.isPreferAttachedBuffers());
132+
DefaultCodecs codecs = new DefaultCodecs(client.getByteBufAllocator(), this.configuration.isPreferAttachedBuffers(),
133+
() -> client.getTimeZone().map(TimeZone::toZoneId).orElse(defaultZone));
129134
StatementCache statementCache = StatementCache.fromPreparedStatementCacheQueries(client, this.configuration.getPreparedStatementCacheQueries());
130135

131136
// early connection object to retrieve initialization details
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.postgresql.codec;
18+
19+
import java.time.ZoneId;
20+
21+
/**
22+
* Configuration object for codecs providing various details required for encoding and decoding data.
23+
*
24+
* @since 1.0
25+
*/
26+
public interface CodecConfiguration {
27+
28+
/**
29+
* Return the zone identifier used for temporal conversions from local to zoned temporals.
30+
*
31+
* @return the zone identifier
32+
*/
33+
ZoneId getZoneId();
34+
35+
}

src/main/java/io/r2dbc/postgresql/codec/DateCodec.java

+9-6
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@
2626
import java.time.LocalDateTime;
2727
import java.time.ZoneId;
2828
import java.util.Date;
29+
import java.util.function.Supplier;
2930

3031
final class DateCodec extends AbstractCodec<Date> implements ArrayCodecDelegate<Date> {
3132

3233
private final LocalDateTimeCodec delegate;
3334

34-
DateCodec(ByteBufAllocator byteBufAllocator) {
35+
private final Supplier<ZoneId> zoneIdSupplier;
36+
37+
DateCodec(ByteBufAllocator byteBufAllocator, Supplier<ZoneId> zoneIdSupplier) {
3538
super(Date.class);
3639

37-
Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
38-
this.delegate = new LocalDateTimeCodec(byteBufAllocator);
40+
this.delegate = new LocalDateTimeCodec(byteBufAllocator, zoneIdSupplier);
41+
this.zoneIdSupplier = Assert.requireNonNull(zoneIdSupplier, "zoneIdSupplier must not be null");
3942
}
4043

4144
@Override
@@ -61,7 +64,7 @@ Date doDecode(ByteBuf buffer, PostgresTypeIdentifier dataType, @Nullable Format
6164
Assert.requireNonNull(buffer, "byteBuf must not be null");
6265

6366
LocalDateTime intermediary = this.delegate.doDecode(buffer, dataType, format, LocalDateTime.class);
64-
return Date.from(intermediary.atZone(ZoneId.systemDefault()).toInstant());
67+
return Date.from(intermediary.atZone(this.zoneIdSupplier.get()).toInstant());
6568
}
6669

6770
@Override
@@ -90,8 +93,8 @@ public PostgresTypeIdentifier getArrayDataType() {
9093
return this.delegate.getArrayDataType();
9194
}
9295

93-
private static LocalDateTime normalize(Date value) {
94-
return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
96+
private LocalDateTime normalize(Date value) {
97+
return value.toInstant().atZone(this.zoneIdSupplier.get()).toLocalDateTime();
9598
}
9699

97100
}

src/main/java/io/r2dbc/postgresql/codec/DefaultCodecs.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.Collections;
3232
import java.util.Iterator;
3333
import java.util.List;
34+
import java.util.TimeZone;
3435
import java.util.concurrent.CopyOnWriteArrayList;
3536
import java.util.function.Function;
3637

@@ -63,33 +64,46 @@ public DefaultCodecs(ByteBufAllocator byteBufAllocator) {
6364
* @param preferAttachedBuffers whether to prefer attached (pooled) {@link ByteBuf buffers}. Use {@code false} (default) to use detached buffers which minimize the risk of memory leaks.
6465
*/
6566
public DefaultCodecs(ByteBufAllocator byteBufAllocator, boolean preferAttachedBuffers) {
66-
this(byteBufAllocator, preferAttachedBuffers, CachedCodecLookup::new);
67+
this(byteBufAllocator, preferAttachedBuffers, () -> TimeZone.getDefault().toZoneId());
6768
}
6869

6970
/**
7071
* Create a new instance of {@link DefaultCodecs}.
7172
*
7273
* @param byteBufAllocator the {@link ByteBufAllocator} to use for encoding
7374
* @param preferAttachedBuffers whether to prefer attached (pooled) {@link ByteBuf buffers}. Use {@code false} (default) to use detached buffers which minimize the risk of memory leaks.
75+
* @param configuration the {@link CodecConfiguration} to use for encoding/decoding
76+
*/
77+
public DefaultCodecs(ByteBufAllocator byteBufAllocator, boolean preferAttachedBuffers, CodecConfiguration configuration) {
78+
this(byteBufAllocator, preferAttachedBuffers, configuration, CachedCodecLookup::new);
79+
}
80+
81+
/**
82+
* Create a new instance of {@link DefaultCodecs}.
83+
*
84+
* @param byteBufAllocator the {@link ByteBufAllocator} to use for encoding
85+
* @param configuration the {@link CodecConfiguration} to use for encoding/decoding
86+
* @param preferAttachedBuffers whether to prefer attached (pooled) {@link ByteBuf buffers}. Use {@code false} (default) to use detached buffers which minimize the risk of memory leaks.
7487
* @param codecLookupFunction provides the {@link CodecLookup} to use for finding relevant codecs
7588
*/
76-
DefaultCodecs(ByteBufAllocator byteBufAllocator, boolean preferAttachedBuffers, Function<CodecRegistry, CodecLookup> codecLookupFunction) {
89+
DefaultCodecs(ByteBufAllocator byteBufAllocator, boolean preferAttachedBuffers, CodecConfiguration configuration, Function<CodecRegistry, CodecLookup> codecLookupFunction) {
7790
Assert.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null");
91+
Assert.requireNonNull(configuration, "configuration must not be null");
7892
Assert.requireNonNull(codecLookupFunction, "codecLookupFunction must not be null");
7993

8094
this.codecLookup = codecLookupFunction.apply(this);
81-
this.codecs = getDefaultCodecs(byteBufAllocator, preferAttachedBuffers);
95+
this.codecs = getDefaultCodecs(byteBufAllocator, preferAttachedBuffers, configuration);
8296
this.codecLookup.afterCodecAdded();
8397
}
8498

8599
@SuppressWarnings({"unchecked", "rawtypes"})
86-
private static List<Codec<?>> getDefaultCodecs(ByteBufAllocator byteBufAllocator, boolean preferAttachedBuffers) {
100+
private static List<Codec<?>> getDefaultCodecs(ByteBufAllocator byteBufAllocator, boolean preferAttachedBuffers, CodecConfiguration configuration) {
87101

88102
List<Codec<?>> codecs = new CopyOnWriteArrayList<>(Arrays.asList(
89103

90104
// Prioritized Codecs
91105
new StringCodec(byteBufAllocator),
92-
new InstantCodec(byteBufAllocator),
106+
new InstantCodec(byteBufAllocator, configuration::getZoneId),
93107
new ZonedDateTimeCodec(byteBufAllocator),
94108
new BinaryByteBufferCodec(byteBufAllocator),
95109
new BinaryByteArrayCodec(byteBufAllocator),
@@ -104,7 +118,7 @@ private static List<Codec<?>> getDefaultCodecs(ByteBufAllocator byteBufAllocator
104118
new IntegerCodec(byteBufAllocator),
105119
new IntervalCodec(byteBufAllocator),
106120
new LocalDateCodec(byteBufAllocator),
107-
new LocalDateTimeCodec(byteBufAllocator),
121+
new LocalDateTimeCodec(byteBufAllocator, configuration::getZoneId),
108122
new LocalTimeCodec(byteBufAllocator),
109123
new LongCodec(byteBufAllocator),
110124
new OffsetDateTimeCodec(byteBufAllocator),
@@ -125,7 +139,7 @@ private static List<Codec<?>> getDefaultCodecs(ByteBufAllocator byteBufAllocator
125139

126140
// Fallback for Object.class
127141
new ByteCodec(byteBufAllocator),
128-
new DateCodec(byteBufAllocator),
142+
new DateCodec(byteBufAllocator, configuration::getZoneId),
129143

130144
new BlobCodec(byteBufAllocator),
131145
new ClobCodec(byteBufAllocator),

src/main/java/io/r2dbc/postgresql/codec/InstantCodec.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,18 @@
2727
import java.time.LocalDateTime;
2828
import java.time.ZoneId;
2929
import java.time.ZoneOffset;
30+
import java.util.function.Supplier;
3031

3132
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.TIMESTAMPTZ;
3233
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.TIMESTAMPTZ_ARRAY;
3334

3435
final class InstantCodec extends AbstractTemporalCodec<Instant> {
3536

36-
InstantCodec(ByteBufAllocator byteBufAllocator) {
37+
private final Supplier<ZoneId> zoneIdSupplier;
38+
39+
InstantCodec(ByteBufAllocator byteBufAllocator, Supplier<ZoneId> zoneIdSupplier) {
3740
super(Instant.class, byteBufAllocator, TIMESTAMPTZ, TIMESTAMPTZ_ARRAY, Instant::toString);
41+
this.zoneIdSupplier = zoneIdSupplier;
3842
}
3943

4044
@Override
@@ -44,11 +48,15 @@ Instant doDecode(ByteBuf buffer, PostgresTypeIdentifier dataType, @Nullable Form
4448
return decodeTemporal(buffer, dataType, format, Instant.class, temporal -> {
4549

4650
if (temporal instanceof LocalDateTime) {
47-
return ((LocalDateTime) temporal).toInstant(ZoneOffset.UTC);
51+
ZoneId zoneId = this.zoneIdSupplier.get();
52+
ZoneOffset offset = zoneId.getRules().getOffset((LocalDateTime) temporal);
53+
return ((LocalDateTime) temporal).toInstant(offset);
4854
}
4955

5056
if (temporal instanceof LocalDate) {
51-
return ((LocalDate) temporal).atStartOfDay(ZoneId.systemDefault()).toInstant();
57+
ZoneId zoneId = this.zoneIdSupplier.get();
58+
59+
return ((LocalDate) temporal).atStartOfDay(zoneId).toInstant();
5260
}
5361

5462
return Instant.from(temporal);

src/main/java/io/r2dbc/postgresql/codec/LocalDateTimeCodec.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,34 @@
2727
import java.time.LocalDateTime;
2828
import java.time.ZoneId;
2929
import java.time.ZoneOffset;
30+
import java.util.function.Supplier;
3031

3132
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.TIMESTAMP;
3233
import static io.r2dbc.postgresql.codec.PostgresqlObjectId.TIMESTAMP_ARRAY;
3334

3435
final class LocalDateTimeCodec extends AbstractTemporalCodec<LocalDateTime> {
3536

36-
LocalDateTimeCodec(ByteBufAllocator byteBufAllocator) {
37+
private final Supplier<ZoneId> zoneIdSupplier;
38+
39+
LocalDateTimeCodec(ByteBufAllocator byteBufAllocator, Supplier<ZoneId> zoneIdSupplier) {
3740
super(LocalDateTime.class, byteBufAllocator, TIMESTAMP, TIMESTAMP_ARRAY, LocalDateTime::toString);
41+
this.zoneIdSupplier = Assert.requireNonNull(zoneIdSupplier, "zoneIdSupplier must not be null");
3842
}
3943

4044
@Override
4145
LocalDateTime doDecode(ByteBuf buffer, PostgresTypeIdentifier dataType, @Nullable Format format, @Nullable Class<? extends LocalDateTime> type) {
4246
Assert.requireNonNull(buffer, "byteBuf must not be null");
4347

4448
return decodeTemporal(buffer, dataType, format, LocalDateTime.class, temporal -> {
49+
ZoneId zone = this.zoneIdSupplier.get();
50+
4551
if (temporal instanceof LocalDate) {
46-
return ((LocalDate) temporal).atStartOfDay(ZoneId.systemDefault()).toLocalDateTime();
52+
return ((LocalDate) temporal).atStartOfDay(zone).toLocalDateTime();
4753
}
4854

49-
return Instant.from(temporal).atOffset(ZoneOffset.UTC).toLocalDateTime();
55+
Instant instant = Instant.from(temporal);
56+
ZoneOffset offset = zone.getRules().getOffset(instant);
57+
return instant.atOffset(offset).toLocalDateTime();
5058
});
5159
}
5260

src/test/java/io/r2dbc/postgresql/codec/DateCodecUnitTests.java

+12-10
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ final class DateCodecUnitTests {
4444

4545
private static final int dataType = TIMESTAMP.getObjectId();
4646

47+
private final DateCodec codec = new DateCodec(TEST, ZoneId::systemDefault);
48+
4749
@Test
4850
void constructorNoByteBufAllocator() {
49-
assertThatIllegalArgumentException().isThrownBy(() -> new DateCodec(null))
51+
assertThatIllegalArgumentException().isThrownBy(() -> new DateCodec(null, null))
5052
.withMessage("byteBufAllocator must not be null");
5153
}
5254

@@ -55,26 +57,26 @@ void decode() {
5557
Instant testInstant = LocalDateTime.parse("2010-02-01T10:08:04.412").atZone(ZoneId.systemDefault()).toInstant();
5658
Date date = Date.from(testInstant);
5759

58-
assertThat(new DateCodec(TEST).decode(encode(TEST, "2010-02-01 10:08:04.412"), dataType, FORMAT_TEXT, Date.class))
60+
assertThat(this.codec.decode(encode(TEST, "2010-02-01 10:08:04.412"), dataType, FORMAT_TEXT, Date.class))
5961
.isEqualTo(date);
6062
}
6163

6264
@Test
6365
void decodeFromDate() {
6466
Date date = Date.from(LocalDateTime.parse("2018-11-04T00:00:00.000").atZone(ZoneId.systemDefault()).toInstant());
6567

66-
assertThat(new DateCodec(TEST).decode(encode(TEST, "2018-11-04"), DATE.getObjectId(), FORMAT_TEXT, Date.class))
68+
assertThat(this.codec.decode(encode(TEST, "2018-11-04"), DATE.getObjectId(), FORMAT_TEXT, Date.class))
6769
.isEqualTo(date);
6870
}
6971

7072
@Test
7173
void decodeNoByteBuf() {
72-
assertThat(new DateCodec(TEST).decode(null, dataType, FORMAT_TEXT, Date.class)).isNull();
74+
assertThat(this.codec.decode(null, dataType, FORMAT_TEXT, Date.class)).isNull();
7375
}
7476

7577
@Test
7678
void doCanDecode() {
77-
DateCodec codec = new DateCodec(TEST);
79+
DateCodec codec = this.codec;
7880

7981
assertThat(codec.doCanDecode(TIMESTAMP, FORMAT_BINARY)).isTrue();
8082
assertThat(codec.doCanDecode(MONEY, FORMAT_TEXT)).isFalse();
@@ -83,13 +85,13 @@ void doCanDecode() {
8385

8486
@Test
8587
void doCanDecodeNoFormat() {
86-
assertThatIllegalArgumentException().isThrownBy(() -> new DateCodec(TEST).doCanDecode(VARCHAR, null))
88+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.doCanDecode(VARCHAR, null))
8789
.withMessage("format must not be null");
8890
}
8991

9092
@Test
9193
void doCanDecodeNoType() {
92-
assertThatIllegalArgumentException().isThrownBy(() -> new DateCodec(TEST).doCanDecode(null, FORMAT_TEXT))
94+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.doCanDecode(null, FORMAT_TEXT))
9395
.withMessage("type must not be null");
9496
}
9597

@@ -98,21 +100,21 @@ void doEncode() {
98100
Instant testInstant = LocalDateTime.parse("2010-02-01T10:08:04.412").atZone(ZoneId.systemDefault()).toInstant();
99101
Date date = Date.from(testInstant);
100102

101-
assertThat(new DateCodec(TEST).doEncode(date))
103+
assertThat(this.codec.doEncode(date))
102104
.hasFormat(FORMAT_TEXT)
103105
.hasType(TIMESTAMP.getObjectId())
104106
.hasValue(encode(TEST, "2010-02-01T10:08:04.412"));
105107
}
106108

107109
@Test
108110
void doEncodeNoValue() {
109-
assertThatIllegalArgumentException().isThrownBy(() -> new DateCodec(TEST).doEncode(null))
111+
assertThatIllegalArgumentException().isThrownBy(() -> this.codec.doEncode(null))
110112
.withMessage("value must not be null");
111113
}
112114

113115
@Test
114116
void encodeNull() {
115-
assertThat(new DateCodec(TEST).encodeNull())
117+
assertThat(this.codec.encodeNull())
116118
.isEqualTo(new EncodedParameter(FORMAT_TEXT, TIMESTAMP.getObjectId(), NULL_VALUE));
117119
}
118120

0 commit comments

Comments
 (0)