From 523cdad66f0568602919000c3ef92b0746cc2d03 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Fri, 27 Mar 2020 15:59:54 -0500 Subject: [PATCH] Truncate nanoseconds in EncodeText for Timestamptz and Timestamp PostgreSQL has microsecond precision. If more than this precision is supplied in the text format it is rounded. This was inconsistent with the binary format. See https://github.com/jackc/pgx/issues/699 for original issue. --- timestamp.go | 2 +- timestamp_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ timestamptz.go | 2 +- timestamptz_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/timestamp.go b/timestamp.go index 35ac5143..de059f7e 100644 --- a/timestamp.go +++ b/timestamp.go @@ -158,7 +158,7 @@ func (src Timestamp) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { switch src.InfinityModifier { case None: - s = src.Time.Format(pgTimestampFormat) + s = src.Time.Truncate(time.Microsecond).Format(pgTimestampFormat) case Infinity: s = "infinity" case NegativeInfinity: diff --git a/timestamp_test.go b/timestamp_test.go index eec0a52e..2fdc7171 100644 --- a/timestamp_test.go +++ b/timestamp_test.go @@ -32,6 +32,51 @@ func TestTimestampTranscode(t *testing.T) { }) } +func TestTimestampNanosecondsTruncated(t *testing.T) { + tests := []struct { + input time.Time + expected time.Time + }{ + {time.Date(2020, 1, 1, 0, 0, 0, 999999999, time.UTC), time.Date(2020, 1, 1, 0, 0, 0, 999999000, time.UTC)}, + {time.Date(2020, 1, 1, 0, 0, 0, 999999001, time.UTC), time.Date(2020, 1, 1, 0, 0, 0, 999999000, time.UTC)}, + } + for i, tt := range tests { + { + ts := pgtype.Timestamp{Time: tt.input, Status: pgtype.Present} + buf, err := ts.EncodeText(nil, nil) + if err != nil { + t.Errorf("%d. EncodeText failed - %v", i, err) + } + + ts.DecodeText(nil, buf) + if err != nil { + t.Errorf("%d. DecodeText failed - %v", i, err) + } + + if !(ts.Status == pgtype.Present && ts.Time.Equal(tt.expected)) { + t.Errorf("%d. EncodeText did not truncate nanoseconds", i) + } + } + + { + ts := pgtype.Timestamp{Time: tt.input, Status: pgtype.Present} + buf, err := ts.EncodeBinary(nil, nil) + if err != nil { + t.Errorf("%d. EncodeBinary failed - %v", i, err) + } + + ts.DecodeBinary(nil, buf) + if err != nil { + t.Errorf("%d. DecodeBinary failed - %v", i, err) + } + + if !(ts.Status == pgtype.Present && ts.Time.Equal(tt.expected)) { + t.Errorf("%d. EncodeBinary did not truncate nanoseconds", i) + } + } + } +} + func TestTimestampSet(t *testing.T) { type _time time.Time diff --git a/timestamptz.go b/timestamptz.go index d390d266..100f44a5 100644 --- a/timestamptz.go +++ b/timestamptz.go @@ -160,7 +160,7 @@ func (src Timestamptz) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { switch src.InfinityModifier { case None: - s = src.Time.UTC().Format(pgTimestamptzSecondFormat) + s = src.Time.UTC().Truncate(time.Microsecond).Format(pgTimestamptzSecondFormat) case Infinity: s = "infinity" case NegativeInfinity: diff --git a/timestamptz_test.go b/timestamptz_test.go index a020b1ec..a088fc08 100644 --- a/timestamptz_test.go +++ b/timestamptz_test.go @@ -32,6 +32,51 @@ func TestTimestamptzTranscode(t *testing.T) { }) } +func TestTimestamptzNanosecondsTruncated(t *testing.T) { + tests := []struct { + input time.Time + expected time.Time + }{ + {time.Date(2020, 1, 1, 0, 0, 0, 999999999, time.Local), time.Date(2020, 1, 1, 0, 0, 0, 999999000, time.Local)}, + {time.Date(2020, 1, 1, 0, 0, 0, 999999001, time.Local), time.Date(2020, 1, 1, 0, 0, 0, 999999000, time.Local)}, + } + for i, tt := range tests { + { + tstz := pgtype.Timestamptz{Time: tt.input, Status: pgtype.Present} + buf, err := tstz.EncodeText(nil, nil) + if err != nil { + t.Errorf("%d. EncodeText failed - %v", i, err) + } + + tstz.DecodeText(nil, buf) + if err != nil { + t.Errorf("%d. DecodeText failed - %v", i, err) + } + + if !(tstz.Status == pgtype.Present && tstz.Time.Equal(tt.expected)) { + t.Errorf("%d. EncodeText did not truncate nanoseconds", i) + } + } + + { + tstz := pgtype.Timestamptz{Time: tt.input, Status: pgtype.Present} + buf, err := tstz.EncodeBinary(nil, nil) + if err != nil { + t.Errorf("%d. EncodeBinary failed - %v", i, err) + } + + tstz.DecodeBinary(nil, buf) + if err != nil { + t.Errorf("%d. DecodeBinary failed - %v", i, err) + } + + if !(tstz.Status == pgtype.Present && tstz.Time.Equal(tt.expected)) { + t.Errorf("%d. EncodeBinary did not truncate nanoseconds", i) + } + } + } +} + func TestTimestamptzSet(t *testing.T) { type _time time.Time