Skip to content

Commit 3dcc148

Browse files
committed
encode: fix 2400 time encoding for time/timetz
Note that Postgres supports 24:00 for both time and timetz operations. When evaluating "24:00" for both Time and TimeTZ datatypes, the time.Time library does not recognise 24 as a legitimate hour. This requires special parsing for it to work. As such, work around the problem by subtracting a day, and adding it back later when we recognize it as 24:00 time.
1 parent c904eab commit 3dcc148

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

encode.go

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"math"
11+
"regexp"
1112
"strconv"
1213
"strings"
1314
"sync"
@@ -16,6 +17,8 @@ import (
1617
"github.com/lib/pq/oid"
1718
)
1819

20+
var time2400Regex = regexp.MustCompile(`^(24:00(?::00(?:\.0+)?)?)(?:[Z+-].*)?$`)
21+
1922
func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte {
2023
switch v := x.(type) {
2124
case []byte:
@@ -202,10 +205,27 @@ func mustParse(f string, typ oid.Oid, s []byte) time.Time {
202205
str[len(str)-3] == ':' {
203206
f += ":00"
204207
}
208+
// Special case for 24:00 time.
209+
// Unfortunately, golang does not parse 24:00 as a proper time.
210+
// In this case, we want to try "round to the next day", to differentiate.
211+
// As such, we find if the 24:00 time matches at the beginning; if so,
212+
// we default it back to 00:00 but add a day later.
213+
var is2400Time bool
214+
switch typ {
215+
case oid.T_timetz, oid.T_time:
216+
if matches := time2400Regex.FindStringSubmatch(str); matches != nil {
217+
// Concatenate timezone information at the back.
218+
str = "00:00:00" + str[len(matches[1]):]
219+
is2400Time = true
220+
}
221+
}
205222
t, err := time.Parse(f, str)
206223
if err != nil {
207224
errorf("decode: %s", err)
208225
}
226+
if is2400Time {
227+
t = t.Add(24 * time.Hour)
228+
}
209229
return t
210230
}
211231

encode_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,85 @@ func TestFormatTsBackend(t *testing.T) {
197197
}
198198
}
199199

200+
func TestTimeWithoutTimezone(t *testing.T) {
201+
db := openTestConn(t)
202+
defer db.Close()
203+
204+
tx, err := db.Begin()
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
defer tx.Rollback()
209+
210+
for _, tc := range []struct {
211+
refTime string
212+
expectedTime time.Time
213+
}{
214+
{"11:59:59", time.Date(0, 1, 1, 11, 59, 59, 0, time.UTC)},
215+
{"24:00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
216+
{"24:00:00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
217+
{"24:00:00.0", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
218+
{"24:00:00.000000", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
219+
} {
220+
t.Run(
221+
fmt.Sprintf("%s => %s", tc.refTime, tc.expectedTime.Format(time.RFC3339)),
222+
func(t *testing.T) {
223+
var gotTime time.Time
224+
row := tx.QueryRow("select $1::time", tc.refTime)
225+
err = row.Scan(&gotTime)
226+
if err != nil {
227+
t.Fatal(err)
228+
}
229+
230+
if !tc.expectedTime.Equal(gotTime) {
231+
t.Errorf("timestamps not equal: %s != %s", tc.expectedTime, gotTime)
232+
}
233+
},
234+
)
235+
}
236+
}
237+
238+
func TestTimeWithTimezone(t *testing.T) {
239+
db := openTestConn(t)
240+
defer db.Close()
241+
242+
tx, err := db.Begin()
243+
if err != nil {
244+
t.Fatal(err)
245+
}
246+
defer tx.Rollback()
247+
248+
for _, tc := range []struct {
249+
refTime string
250+
expectedTime time.Time
251+
}{
252+
{"11:59:59+00:00", time.Date(0, 1, 1, 11, 59, 59, 0, time.UTC)},
253+
{"11:59:59+04:00", time.Date(0, 1, 1, 11, 59, 59, 0, time.FixedZone("+04", 4*60*60))},
254+
{"24:00+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
255+
{"24:00Z", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
256+
{"24:00-04:00", time.Date(0, 1, 2, 0, 0, 0, 0, time.FixedZone("-04", -4*60*60))},
257+
{"24:00:00+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
258+
{"24:00:00.0+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
259+
{"24:00:00.000000+00", time.Date(0, 1, 2, 0, 0, 0, 0, time.UTC)},
260+
} {
261+
t.Run(
262+
fmt.Sprintf("%s => %s", tc.refTime, tc.expectedTime.Format(time.RFC3339)),
263+
func(t *testing.T) {
264+
var gotTime time.Time
265+
row := tx.QueryRow("select $1::timetz", tc.refTime)
266+
err = row.Scan(&gotTime)
267+
if err != nil {
268+
t.Fatal(err)
269+
}
270+
271+
if !tc.expectedTime.Equal(gotTime) {
272+
t.Errorf("timestamps not equal: %s != %s", tc.expectedTime, gotTime)
273+
}
274+
},
275+
)
276+
}
277+
}
278+
200279
func TestTimestampWithTimeZone(t *testing.T) {
201280
db := openTestConn(t)
202281
defer db.Close()

0 commit comments

Comments
 (0)