From de8c140cfb98b7b047d53c5718ccbf12eaf813a1 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 25 Feb 2017 19:32:22 -0600 Subject: [PATCH] Add timestamptz null and infinity --- pgtype/pgtype_test.go | 8 ++- pgtype/timestamptz.go | 119 ++++++++++++++++++++++++++++++------- pgtype/timestamptz_test.go | 60 +++++++++++++++++++ values.go | 14 ++++- 4 files changed, 175 insertions(+), 26 deletions(-) create mode 100644 pgtype/timestamptz_test.go diff --git a/pgtype/pgtype_test.go b/pgtype/pgtype_test.go index 025533b3..f4bb6f1d 100644 --- a/pgtype/pgtype_test.go +++ b/pgtype/pgtype_test.go @@ -62,6 +62,12 @@ func forceEncoder(e interface{}, formatCode int16) interface{} { } func testSuccessfulTranscode(t testing.TB, pgTypeName string, values []interface{}) { + testSuccessfulTranscodeEqFunc(t, pgTypeName, values, func(a, b interface{}) bool { + return reflect.DeepEqual(a, b) + }) +} + +func testSuccessfulTranscodeEqFunc(t testing.TB, pgTypeName string, values []interface{}, eqFunc func(a, b interface{}) bool) { conn := mustConnectPgx(t) defer mustClose(t, conn) @@ -87,7 +93,7 @@ func testSuccessfulTranscode(t testing.TB, pgTypeName string, values []interface t.Errorf("%v %d: %v", fc.name, i, err) } - if !reflect.DeepEqual(result.Elem().Interface(), v) { + if !eqFunc(result.Elem().Interface(), v) { t.Errorf("%v %d: expected %v, got %v", fc.name, i, v, result.Interface()) } } diff --git a/pgtype/timestamptz.go b/pgtype/timestamptz.go index 99e7e614..2c4e06cc 100644 --- a/pgtype/timestamptz.go +++ b/pgtype/timestamptz.go @@ -8,13 +8,36 @@ import ( "github.com/jackc/pgx/pgio" ) -const pgTimestamptzFormat = "2006-01-02 15:04:05.999999999Z07:00" +const pgTimestamptzHourFormat = "2006-01-02 15:04:05.999999999Z07" +const pgTimestamptzMinuteFormat = "2006-01-02 15:04:05.999999999Z07:00" +const pgTimestamptzSecondFormat = "2006-01-02 15:04:05.999999999Z07:00:00" const microsecFromUnixEpochToY2K = 946684800 * 1000000 +const ( + negativeInfinityMicrosecondOffset = -9223372036854775808 + infinityMicrosecondOffset = 9223372036854775807 +) + type Timestamptz struct { - // time.Time is embedded to handle Infinity and -Infinity - // TODO - infinity - t time.Time + Time time.Time + Status Status + InfinityModifier +} + +func (t *Timestamptz) ConvertFrom(src interface{}) error { + switch value := src.(type) { + case Timestamptz: + *t = value + case time.Time: + *t = Timestamptz{Time: value, Status: Present} + default: + if originalSrc, ok := underlyingTimeType(src); ok { + return t.ConvertFrom(originalSrc) + } + return fmt.Errorf("cannot convert %v to Timestamptz", value) + } + + return nil } func (t *Timestamptz) DecodeText(r io.Reader) error { @@ -24,7 +47,8 @@ func (t *Timestamptz) DecodeText(r io.Reader) error { } if size == -1 { - return fmt.Errorf("invalid length for timestamptz: %v", size) + *t = Timestamptz{Status: Null} + return nil } buf := make([]byte, int(size)) @@ -33,9 +57,28 @@ func (t *Timestamptz) DecodeText(r io.Reader) error { return err } - t.t, err = time.Parse(pgTimestamptzFormat, string(buf)) - if err != nil { - return err + sbuf := string(buf) + switch sbuf { + case "infinity": + *t = Timestamptz{Status: Present, InfinityModifier: Infinity} + case "-infinity": + *t = Timestamptz{Status: Present, InfinityModifier: -Infinity} + default: + var format string + if sbuf[len(sbuf)-9] == '-' || sbuf[len(sbuf)-9] == '+' { + format = pgTimestamptzSecondFormat + } else if sbuf[len(sbuf)-6] == '-' || sbuf[len(sbuf)-6] == '+' { + format = pgTimestamptzMinuteFormat + } else { + format = pgTimestamptzHourFormat + } + + tim, err := time.Parse(format, sbuf) + if err != nil { + return err + } + + *t = Timestamptz{Time: tim, Status: Present} } return nil @@ -47,6 +90,11 @@ func (t *Timestamptz) DecodeBinary(r io.Reader) error { return err } + if size == -1 { + *t = Timestamptz{Status: Null} + return nil + } + if size != 8 { return fmt.Errorf("invalid length for timestamptz: %v", size) } @@ -56,41 +104,66 @@ func (t *Timestamptz) DecodeBinary(r io.Reader) error { return err } - microsecSinceUnixEpoch := microsecFromUnixEpochToY2K + microsecSinceY2K - t.t = time.Unix(microsecSinceUnixEpoch/1000000, (microsecSinceUnixEpoch%1000000)*1000) + switch microsecSinceY2K { + case infinityMicrosecondOffset: + *t = Timestamptz{Status: Present, InfinityModifier: Infinity} + case negativeInfinityMicrosecondOffset: + *t = Timestamptz{Status: Present, InfinityModifier: -Infinity} + default: + microsecSinceUnixEpoch := microsecFromUnixEpochToY2K + microsecSinceY2K + tim := time.Unix(microsecSinceUnixEpoch/1000000, (microsecSinceUnixEpoch%1000000)*1000) + *t = Timestamptz{Time: tim, Status: Present} + } return nil } func (t Timestamptz) EncodeText(w io.Writer) error { - buf := []byte(t.t.Format(pgTimestamptzFormat)) + if done, err := encodeNotPresent(w, t.Status); done { + return err + } - _, err := pgio.WriteInt32(w, int32(len(buf))) + var s string + + switch t.InfinityModifier { + case None: + s = t.Time.UTC().Format(pgTimestamptzSecondFormat) + case Infinity: + s = "infinity" + case NegativeInfinity: + s = "-infinity" + } + + _, err := pgio.WriteInt32(w, int32(len(s))) if err != nil { return nil } - _, err = w.Write(buf) + _, err = w.Write([]byte(s)) return err } func (t Timestamptz) EncodeBinary(w io.Writer) error { + if done, err := encodeNotPresent(w, t.Status); done { + return err + } + _, err := pgio.WriteInt32(w, 8) if err != nil { return err } - microsecSinceUnixEpoch := t.t.Unix()*1000000 + int64(t.t.Nanosecond())/1000 - microsecSinceY2K := microsecSinceUnixEpoch - microsecFromUnixEpochToY2K + var microsecSinceY2K int64 + switch t.InfinityModifier { + case None: + microsecSinceUnixEpoch := t.Time.Unix()*1000000 + int64(t.Time.Nanosecond())/1000 + microsecSinceY2K = microsecSinceUnixEpoch - microsecFromUnixEpochToY2K + case Infinity: + microsecSinceY2K = infinityMicrosecondOffset + case NegativeInfinity: + microsecSinceY2K = negativeInfinityMicrosecondOffset + } _, err = pgio.WriteInt64(w, microsecSinceY2K) return err } - -func (t Timestamptz) Time() time.Time { - return t.t -} - -func TimestamptzFromTime(t time.Time) Timestamptz { - return Timestamptz{t: t} -} diff --git a/pgtype/timestamptz_test.go b/pgtype/timestamptz_test.go new file mode 100644 index 00000000..e650ffbb --- /dev/null +++ b/pgtype/timestamptz_test.go @@ -0,0 +1,60 @@ +package pgtype_test + +import ( + "testing" + "time" + + "github.com/jackc/pgx/pgtype" +) + +func TestTimestamptzTranscode(t *testing.T) { + testSuccessfulTranscodeEqFunc(t, "timestamptz", []interface{}{ + pgtype.Timestamptz{Time: time.Date(1800, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(1900, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(1905, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(1940, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(1960, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(1999, 12, 31, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(2000, 1, 2, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Time: time.Date(2200, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}, + pgtype.Timestamptz{Status: pgtype.Null}, + pgtype.Timestamptz{Status: pgtype.Present, InfinityModifier: pgtype.Infinity}, + pgtype.Timestamptz{Status: pgtype.Present, InfinityModifier: -pgtype.Infinity}, + }, func(a, b interface{}) bool { + at := a.(pgtype.Timestamptz) + bt := b.(pgtype.Timestamptz) + + return at.Time.Equal(bt.Time) && at.Status == bt.Status && at.InfinityModifier == bt.InfinityModifier + }) +} + +func TestTimestamptzConvertFrom(t *testing.T) { + type _time time.Time + + successfulTests := []struct { + source interface{} + result pgtype.Timestamptz + }{ + {source: time.Date(1900, 1, 1, 0, 0, 0, 0, time.Local), result: pgtype.Timestamptz{Time: time.Date(1900, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}}, + {source: time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local), result: pgtype.Timestamptz{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}}, + {source: time.Date(1999, 12, 31, 12, 59, 59, 0, time.Local), result: pgtype.Timestamptz{Time: time.Date(1999, 12, 31, 12, 59, 59, 0, time.Local), Status: pgtype.Present}}, + {source: time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), result: pgtype.Timestamptz{Time: time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}}, + {source: time.Date(2000, 1, 1, 0, 0, 1, 0, time.Local), result: pgtype.Timestamptz{Time: time.Date(2000, 1, 1, 0, 0, 1, 0, time.Local), Status: pgtype.Present}}, + {source: time.Date(2200, 1, 1, 0, 0, 0, 0, time.Local), result: pgtype.Timestamptz{Time: time.Date(2200, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}}, + {source: _time(time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local)), result: pgtype.Timestamptz{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.Local), Status: pgtype.Present}}, + } + + for i, tt := range successfulTests { + var d pgtype.Timestamptz + err := d.ConvertFrom(tt.source) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if d != tt.result { + t.Errorf("%d: expected %v to convert to %v, but it was %v", i, tt.source, tt.result, d) + } + } +} diff --git a/values.go b/values.go index 0367a71c..90391f29 100644 --- a/values.go +++ b/values.go @@ -2038,7 +2038,12 @@ func encodeTime(w *WriteBuf, oid OID, value time.Time) error { } return d.EncodeBinary(w) case TimestampTzOID, TimestampOID: - return pgtype.TimestamptzFromTime(value).EncodeBinary(w) + var t pgtype.Timestamptz + err := t.ConvertFrom(value) + if err != nil { + return err + } + return t.EncodeBinary(w) default: return fmt.Errorf("cannot encode %s into oid %v", "time.Time", oid) } @@ -2078,7 +2083,12 @@ func decodeTimestampTz(vr *ValueReader) time.Time { return time.Time{} } - return t.Time() + if t.Status == pgtype.Null { + vr.Fatal(ProtocolError("Cannot decode null into time.Time")) + return time.Time{} + } + + return t.Time } func decodeTimestamp(vr *ValueReader) time.Time {