Skip to content

Commit 051ca23

Browse files
authored
Make sure to handle microsecond resolution as PostgreSQL does (#1575)
* Make sure to handle microsecond resolution as PostgreSQL does Fixes #1165 PostgreSQL has microsecond resolution for timestamps. When converting from text, if the nanoseconds part after microseconds is strictly bigger than 499, PostgreSQL server rounds up to the next microsecond. Signed-off-by: Thomas Segismont <[email protected]> * Fix DateTimeTypesExtendedCodec tests Use the timestamp min/max values from PostgreSQL server Then, it's safe to add microseconds to the computed timestamps because we're well under the limit of LocalDateTime.MAX Signed-off-by: Thomas Segismont <[email protected]> --------- Signed-off-by: Thomas Segismont <[email protected]>
1 parent a5de3e7 commit 051ca23

File tree

2 files changed

+73
-29
lines changed

2 files changed

+73
-29
lines changed

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
import io.netty.buffer.Unpooled;
2323
import io.netty.handler.codec.DecoderException;
2424
import io.vertx.core.buffer.Buffer;
25+
import io.vertx.core.internal.buffer.BufferInternal;
2526
import io.vertx.core.internal.logging.Logger;
2627
import io.vertx.core.internal.logging.LoggerFactory;
2728
import io.vertx.core.json.Json;
2829
import io.vertx.core.json.JsonArray;
2930
import io.vertx.core.json.JsonObject;
30-
import io.vertx.core.internal.buffer.BufferInternal;
3131
import io.vertx.pgclient.data.*;
3232
import io.vertx.pgclient.impl.util.UTF8StringEndDetector;
3333
import io.vertx.sqlclient.Tuple;
@@ -42,18 +42,17 @@
4242
import java.nio.charset.StandardCharsets;
4343
import java.text.DecimalFormat;
4444
import java.time.*;
45-
import java.time.format.DateTimeFormatter;
4645
import java.time.format.DateTimeFormatterBuilder;
4746
import java.time.temporal.ChronoField;
48-
import java.time.temporal.ChronoUnit;
4947
import java.util.ArrayList;
5048
import java.util.List;
51-
import java.util.Locale;
5249
import java.util.UUID;
5350
import java.util.function.IntFunction;
5451

5552
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
5653
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
54+
import static java.time.temporal.ChronoUnit.DAYS;
55+
import static java.time.temporal.ChronoUnit.MICROS;
5756

5857
/**
5958
* @author <a href="mailto:[email protected]">Julien Viet</a>
@@ -91,8 +90,7 @@ public class DataTypeCodec {
9190
private static final Float[] empty_float_array = new Float[0];
9291
private static final Double[] empty_double_array = new Double[0];
9392
private static final LocalDate LOCAL_DATE_EPOCH = LocalDate.of(2000, 1, 1);
94-
private static final LocalDateTime LOCAL_DATE_TIME_EPOCH = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
95-
private static final OffsetDateTime OFFSET_DATE_TIME_EPOCH = LocalDateTime.of(2000, 1, 1, 0, 0, 0).atOffset(ZoneOffset.UTC);
93+
private static final LocalDateTime LOCAL_DATE_TIME_EPOCH = LOCAL_DATE_EPOCH.atStartOfDay();
9694
private static final Inet[] empty_inet_array = new Inet[0];
9795
private static final Money[] empty_money_array = new Money[0];
9896

@@ -1017,7 +1015,7 @@ private static void binaryEncodeDATE(LocalDate value, ByteBuf buff) {
10171015
} else if (value == LocalDate.MIN) {
10181016
v = Integer.MIN_VALUE;
10191017
} else {
1020-
v = (int) -value.until(LOCAL_DATE_EPOCH, ChronoUnit.DAYS);
1018+
v = (int) -value.until(LOCAL_DATE_EPOCH, DAYS);
10211019
}
10221020
buff.writeInt(v);
10231021
}
@@ -1030,7 +1028,7 @@ private static LocalDate binaryDecodeDATE(int index, int len, ByteBuf buff) {
10301028
case Integer.MIN_VALUE:
10311029
return LocalDate.MIN;
10321030
default:
1033-
return LOCAL_DATE_EPOCH.plus(val, ChronoUnit.DAYS);
1031+
return LOCAL_DATE_EPOCH.plus(val, DAYS);
10341032
}
10351033
}
10361034

@@ -1079,30 +1077,45 @@ private static OffsetTime textDecodeTIMETZ(int index, int len, ByteBuf buff) {
10791077
return OffsetTime.parse(cs, TIMETZ_FORMAT);
10801078
}
10811079

1082-
// 294277-01-09 04:00:54.775807
1083-
public static final LocalDateTime LDT_PLUS_INFINITY = LOCAL_DATE_TIME_EPOCH.plus(Long.MAX_VALUE, ChronoUnit.MICROS);
1084-
// 4714-11-24 00:00:00 BC
1085-
public static final LocalDateTime LDT_MINUS_INFINITY = LocalDateTime.parse("4714-11-24 00:00:00 BC",
1086-
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss G", Locale.US));
1080+
/*
1081+
* See limits for dates and timestamps
1082+
* https://github.com/postgres/postgres/blob/75e82b2f5a6f5de6b42dbd9ea73be5ff36a931b1/src/include/datatype/timestamp.h
1083+
*
1084+
* In Pg Client, we define inclusive min and max.
1085+
*/
1086+
private static final long MIN_TIMESTAMP = -211813488000000000L;
1087+
private static final LocalDateTime MIN_LDT = LOCAL_DATE_TIME_EPOCH.plus(MIN_TIMESTAMP, MICROS);
1088+
private static final long MAX_TIMESTAMP = 9223371331200000000L - 1;
1089+
private static final LocalDateTime MAX_LDT = LOCAL_DATE_TIME_EPOCH.plus(MAX_TIMESTAMP, MICROS);
10871090

10881091
private static void binaryEncodeTIMESTAMP(LocalDateTime value, ByteBuf buff) {
1089-
if (value.compareTo(LDT_PLUS_INFINITY) >= 0) {
1090-
value = LDT_PLUS_INFINITY;
1091-
} else if (value.compareTo(LDT_MINUS_INFINITY) <= 0) {
1092-
value = LDT_MINUS_INFINITY;
1092+
long timestamp;
1093+
if (MIN_LDT.isAfter(value)) {
1094+
timestamp = MIN_TIMESTAMP;
1095+
} else if (value.isAfter(MAX_LDT)) {
1096+
timestamp = MAX_TIMESTAMP;
1097+
} else {
1098+
// Make sure to handle microsecond resolution as PostgreSQL does when it converts textual representation of timestamps.
1099+
// Over 499 nanos after the microsecond, round up to the next microsecond.
1100+
// Otherwise, do nothing and the nanos after the microsecond will be truncated below.
1101+
int nanosAfterMicro = value.getNano() % 1000;
1102+
if (nanosAfterMicro > 499) {
1103+
value = value.plusNanos(1000 - nanosAfterMicro);
1104+
}
1105+
timestamp = Math.min(MAX_TIMESTAMP, -value.until(LOCAL_DATE_TIME_EPOCH, MICROS));
10931106
}
1094-
buff.writeLong(-value.until(LOCAL_DATE_TIME_EPOCH, ChronoUnit.MICROS));
1107+
buff.writeLong(timestamp);
10951108
}
10961109

10971110
private static LocalDateTime binaryDecodeTIMESTAMP(int index, int len, ByteBuf buff) {
1098-
LocalDateTime val = LOCAL_DATE_TIME_EPOCH.plus(buff.getLong(index), ChronoUnit.MICROS);
1099-
if (LDT_PLUS_INFINITY.equals(val)) {
1100-
return LocalDateTime.MAX;
1101-
} else if (LDT_MINUS_INFINITY.equals(val)) {
1111+
long timestamp = buff.getLong(index);
1112+
if (timestamp <= MIN_TIMESTAMP) {
11021113
return LocalDateTime.MIN;
1103-
} else {
1104-
return val;
11051114
}
1115+
if (timestamp >= MAX_TIMESTAMP) {
1116+
return LocalDateTime.MAX;
1117+
}
1118+
return LOCAL_DATE_TIME_EPOCH.plus(timestamp, MICROS);
11061119
}
11071120

11081121
private static LocalDateTime textDecodeTIMESTAMP(int index, int len, ByteBuf buff) {
@@ -1132,12 +1145,12 @@ private static OffsetDateTime binaryDecodeTIMESTAMPTZ(int index, int len, ByteBu
11321145
private static void binaryEncodeTIMESTAMPTZ(OffsetDateTime value, ByteBuf buff) {
11331146
LocalDateTime ldt;
11341147
if (value.getOffset() != ZoneOffset.UTC) {
1135-
OffsetDateTime max = OffsetDateTime.of(LDT_PLUS_INFINITY, ZoneOffset.UTC);
1136-
if (value.compareTo(max) >= 0) {
1148+
OffsetDateTime max = OffsetDateTime.of(MAX_LDT, ZoneOffset.UTC);
1149+
if (!value.isBefore(max)) {
11371150
ldt = LocalDateTime.MAX;
11381151
} else {
1139-
OffsetDateTime min = OffsetDateTime.of(LDT_MINUS_INFINITY, ZoneOffset.UTC);
1140-
if (value.compareTo(min) <= 0) {
1152+
OffsetDateTime min = OffsetDateTime.of(MIN_LDT, ZoneOffset.UTC);
1153+
if (!value.isAfter(min)) {
11411154
ldt = LocalDateTime.MIN;
11421155
} else {
11431156
ldt = value.toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime();

vertx-pg-client/src/test/java/io/vertx/tests/pgclient/data/DateTimeTypesExtendedCodecTest.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import io.vertx.ext.unit.TestContext;
55
import io.vertx.pgclient.PgConnection;
66
import io.vertx.pgclient.data.Interval;
7-
import io.vertx.tests.sqlclient.ColumnChecker;
87
import io.vertx.sqlclient.Row;
98
import io.vertx.sqlclient.Tuple;
9+
import io.vertx.tests.sqlclient.ColumnChecker;
1010
import org.junit.Test;
1111

1212
import java.time.*;
@@ -628,4 +628,35 @@ private <T> void testDecodeDataTimeGeneric(TestContext ctx,
628628
}));
629629
}));
630630
}
631+
632+
@Test
633+
public void testClientHandlesTimestampResolutionLikeServer(TestContext ctx) {
634+
// PostgreSQL has microsecond resolution for timestamps.
635+
// When converting from text, if the nanoseconds part after microseconds is strictly bigger than 499,
636+
// PostgreSQL server rounds up to the next microsecond.
637+
Async over499 = ctx.async();
638+
PgConnection.connect(vertx, options).onComplete(ctx.asyncAssertSuccess(conn -> {
639+
conn.prepare("SELECT '2025-12-31 23:59:59.999999773'::timestamp WHERE '2025-12-31 23:59:59.999999773'::timestamp = $1::timestamp").onComplete(
640+
ctx.asyncAssertSuccess(p -> {
641+
p.query()
642+
.execute(Tuple.of(LocalDateTime.parse("2025-12-31T23:59:59.999999773"))).onComplete(ctx.asyncAssertSuccess(result -> {
643+
ctx.assertEquals(1, result.size());
644+
ctx.assertEquals(LocalDateTime.parse("2026-01-01T00:00:00.000000"), result.iterator().next().getLocalDateTime(0));
645+
over499.complete();
646+
}));
647+
}));
648+
}));
649+
Async under499 = ctx.async();
650+
PgConnection.connect(vertx, options).onComplete(ctx.asyncAssertSuccess(conn -> {
651+
conn.prepare("SELECT '2025-12-31 23:59:59.999999227'::timestamp WHERE '2025-12-31 23:59:59.999999227'::timestamp = $1::timestamp").onComplete(
652+
ctx.asyncAssertSuccess(p -> {
653+
p.query()
654+
.execute(Tuple.of(LocalDateTime.parse("2025-12-31T23:59:59.999999227"))).onComplete(ctx.asyncAssertSuccess(result -> {
655+
ctx.assertEquals(1, result.size());
656+
ctx.assertEquals(LocalDateTime.parse("2025-12-31T23:59:59.999999"), result.iterator().next().getLocalDateTime(0));
657+
under499.complete();
658+
}));
659+
}));
660+
}));
661+
}
631662
}

0 commit comments

Comments
 (0)