Add timestamptz null and infinity

pgxtype-experiment2
Jack Christensen 2017-02-25 19:32:22 -06:00
parent 7d9f954de4
commit de8c140cfb
4 changed files with 175 additions and 26 deletions

View File

@ -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())
}
}

View File

@ -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}
}

View File

@ -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)
}
}
}

View File

@ -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 {