Skip to content

Commit c05568b

Browse files
committed
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 <tsegismont@gmail.com>
1 parent d34d5dc commit c05568b

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

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

Lines changed: 8 additions & 1 deletion
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;
@@ -1086,6 +1086,13 @@ private static OffsetTime textDecodeTIMETZ(int index, int len, ByteBuf buff) {
10861086
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss G", Locale.US));
10871087

10881088
private static void binaryEncodeTIMESTAMP(LocalDateTime value, ByteBuf buff) {
1089+
// Make sure to handle microsecond resolution as PostgreSQL does when it converts textual representation of timestamps.
1090+
// Over 499 nanos after the microsecond, round up to the next microsecond.
1091+
// Otherwise, do nothing and the nanos after the microsecond will be truncated below.
1092+
int nanosAfterMicro = value.getNano() % 1000;
1093+
if (nanosAfterMicro > 499) {
1094+
value = value.plusNanos(1000 - nanosAfterMicro);
1095+
}
10891096
if (value.compareTo(LDT_PLUS_INFINITY) >= 0) {
10901097
value = LDT_PLUS_INFINITY;
10911098
} else if (value.compareTo(LDT_MINUS_INFINITY) <= 0) {

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)