Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 92 additions & 1 deletion encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{}
case oid.T_bytea:
return parseBytea(s)
case oid.T_timestamptz:
return parseTs(parameterStatus.currentLocation, string(s))
return decodeTimestamptzISO(s, parameterStatus.currentLocation)
case oid.T_timestamp, oid.T_date:
return parseTs(nil, string(s))
case oid.T_time:
Expand Down Expand Up @@ -207,6 +207,97 @@ func (c *locationCache) getLocation(offset int) *time.Location {
return location
}

// Decode a Time from the "ISO" format.
func decodeTimestamptzISO(src []byte, sessionLocation *time.Location) time.Time {
atoi := func(s []byte) (result int) {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
errorf("unable to parse timestamptz; expected number at %q", s)
}
result = result*10 + int(s[i]-'0')
}
return
}

// Asserts a separator then converts the remaining digits.
readDigits := func(sep byte, s []byte) int {
if s[0] != sep {
errorf("unable to parse timestamptz; expected '%v' at %q", sep, s)
}
return atoi(s[1:])
}

sepYearMonth := bytes.IndexByte(src, '-')
year := atoi(src[:sepYearMonth])
src = src[sepYearMonth:]

// Time before current era is suffixed with BC
if src[len(src)-1] == 'C' {
// Negate the year and add one.
// See http://www.postgresql.org/docs/current/static/datetime-input-rules.html
year = 1 - year

// Strip " BC"
src = src[:len(src)-3]
}

month := readDigits('-', src[0:3])
day := readDigits('-', src[3:6])
hour := readDigits(' ', src[6:9])
minute := readDigits(':', src[9:12])
second := readDigits(':', src[12:15])
src = src[15:]

// Offset from UTC is formatted ±hh[:mm[:ss]]
offset := 0
switch {
case len(src) > 6 && src[len(src)-6] == ':':
offset += readDigits(':', src[len(src)-3:])
src = src[:len(src)-3]
fallthrough

case len(src) > 3 && src[len(src)-3] == ':':
offset += 60 * readDigits(':', src[len(src)-3:])
src = src[:len(src)-3]
}

if src[len(src)-3] == '+' {
offset += 3600 * readDigits('+', src[len(src)-3:])
} else {
offset += 3600 * readDigits('-', src[len(src)-3:])
offset = -offset
}
src = src[:len(src)-3]

// Fractional seconds
nanosecond := 0
if len(src) > 1 {
nanosecond = readDigits('.', src)

// Scale to nanosecnds
for i := len(src); i < 10; i++ {
nanosecond *= 10
}
}

result := time.Date(
year, time.Month(month), day,
hour, minute, second, nanosecond,
globalLocationCache.getLocation(offset))

if sessionLocation != nil {
// Set the location based on session TimeZone, but only when it reports
// the same offset from UTC.
sessionTime := result.In(sessionLocation)
_, sessionOffset := sessionTime.Zone()
if sessionOffset == offset {
result = sessionTime
}
}

return result
}

// This is a time function specific to the Postgres default DateStyle
// setting ("ISO, MDY"), the only one we currently support. This
// accounts for the discrepancies between the parsing available with
Expand Down
86 changes: 86 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,92 @@ func TestParseTs(t *testing.T) {
}
}

type timeTest struct {
raw time.Time
fromBackend []byte
toBackend []byte
}

var timestamptzISOTests = []timeTest{
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06.123456789+00`),
[]byte(`0001-02-03T04:05:06.123456789Z`)},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)),
[]byte(`0001-02-03 04:05:06.123456789+02`),
[]byte(`0001-02-03T04:05:06.123456789+02:00`)},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)),
[]byte(`0001-02-03 04:05:06.123456789-06`),
[]byte(`0001-02-03T04:05:06.123456789-06:00`)},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 7*60*60+30*60+9)),
[]byte(`0001-02-03 04:05:06.123456789+07:30:09`),
[]byte(`0001-02-03T04:05:06.123456789+07:30:09`)},

{time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06+00`),
[]byte(`0001-02-03T04:05:06Z`)},
{time.Date(1, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06.000001+00`),
[]byte(`0001-02-03T04:05:06.000001Z`)},
{time.Date(1, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06.001+00`),
[]byte(`0001-02-03T04:05:06.001Z`)},

{time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)),
[]byte(`10000-02-03 04:05:06.123456789+00`),
[]byte(`10000-02-03T04:05:06.123456789Z`)},
{time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)),
[]byte(`10000-02-03 04:05:06.123456789+02`),
[]byte(`10000-02-03T04:05:06.123456789+02:00`)},
{time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)),
[]byte(`10000-02-03 04:05:06.123456789-06`),
[]byte(`10000-02-03T04:05:06.123456789-06:00`)},
{time.Date(10000, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 7*60*60+30*60+9)),
[]byte(`10000-02-03 04:05:06.123456789+07:30:09`),
[]byte(`10000-02-03T04:05:06.123456789+07:30:09`)},

{time.Date(10000, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0)),
[]byte(`10000-02-03 04:05:06+00`),
[]byte(`10000-02-03T04:05:06Z`)},
{time.Date(10000, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0)),
[]byte(`10000-02-03 04:05:06.000001+00`),
[]byte(`10000-02-03T04:05:06.000001Z`)},
{time.Date(10000, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0)),
[]byte(`10000-02-03 04:05:06.001+00`),
[]byte(`10000-02-03T04:05:06.001Z`)},

{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06.123456789+00 BC`),
[]byte(`0001-02-03T04:05:06.123456789Z BC`)},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)),
[]byte(`0001-02-03 04:05:06.123456789+02 BC`),
[]byte(`0001-02-03T04:05:06.123456789+02:00 BC`)},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)),
[]byte(`0001-02-03 04:05:06.123456789-06 BC`),
[]byte(`0001-02-03T04:05:06.123456789-06:00 BC`)},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 7*60*60+30*60+9)),
[]byte(`0001-02-03 04:05:06.123456789+07:30:09 BC`),
[]byte(`0001-02-03T04:05:06.123456789+07:30:09 BC`)},

{time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06+00 BC`),
[]byte(`0001-02-03T04:05:06Z BC`)},
{time.Date(0, time.February, 3, 4, 5, 6, 1000, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06.000001+00 BC`),
[]byte(`0001-02-03T04:05:06.000001Z BC`)},
{time.Date(0, time.February, 3, 4, 5, 6, 1000000, time.FixedZone("", 0)),
[]byte(`0001-02-03 04:05:06.001+00 BC`),
[]byte(`0001-02-03T04:05:06.001Z BC`)},
}

func TestDecodeTimestamptzISO(t *testing.T) {
for _, tt := range timestamptzISOTests {
result := decodeTimestamptzISO(tt.fromBackend, nil)
if !tt.raw.Equal(result) || tt.raw.Format("-0700 MST") != result.Format("-0700 MST") {
t.Errorf("Expected %v, got %v", tt.raw, result)
}
}
}

// Now test that sending the value into the database and parsing it back
// returns the same time.Time value.
func TestEncodeAndParseTs(t *testing.T) {
Expand Down