Skip to content

Commit 981a7ac

Browse files
committed
datetime: add TZ support
The patch adds timezone support [1] for datetime. For the purpose of compatibility we got constants for timezones from Tarantool [2]. The patch adds a script to generate the data according to the instructions [3]. 1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#timezone-support 2. https://github.com/tarantool/tarantool/blob/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/timezones.h 3. https://github.com/tarantool/tarantool/blob/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/gen-zone-abbrevs.pl#L35-L39 Closes #163
1 parent 609268f commit 981a7ac

8 files changed

+1793
-60
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1717
- Support datetime type in msgpack (#118)
1818
- Prepared SQL statements (#117)
1919
- Context support for request objects (#48)
20+
- TZ support for datetime (#163)
2021

2122
### Changed
2223

Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ clean:
2424
deps: clean
2525
( cd ./queue; tarantoolctl rocks install queue 1.1.0 )
2626

27+
.PHONY: datetime-timezones
28+
datetime-timezones:
29+
(cd ./datetime; ./gen-timezones.sh)
30+
2731
.PHONY: format
2832
format:
2933
goimports -l -w .

datetime/datetime.go

+47-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package datetime
1212
import (
1313
"encoding/binary"
1414
"fmt"
15+
"math"
1516
"time"
1617

1718
"gopkg.in/vmihailenco/msgpack.v2"
@@ -81,6 +82,16 @@ type Datetime struct {
8182
time time.Time
8283
}
8384

85+
const (
86+
noTimezoneZone = ""
87+
noTimezoneOffset = 0
88+
)
89+
90+
// NoTimezone allows to create a datetime without UTC timezone for
91+
// Tarantool. The problem is that Golang by default creates a time value with
92+
// UTC timezone. So it is a way to create a datetime without timezone.
93+
var NoTimezone = time.FixedZone(noTimezoneZone, noTimezoneOffset)
94+
8495
// NewDatetime returns a pointer to a new datetime.Datetime that contains a
8596
// specified time.Time. It may returns an error if the Time value is out of
8697
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z]
@@ -91,6 +102,18 @@ func NewDatetime(t time.Time) (*Datetime, error) {
91102
return nil, fmt.Errorf("Time %s is out of supported range.", t)
92103
}
93104

105+
zone, offset := t.Zone()
106+
if zone != noTimezoneZone {
107+
if _, ok := timezoneToIndex[zone]; !ok {
108+
return nil, fmt.Errorf("Unknown timezone %s with offset %d",
109+
zone, offset)
110+
}
111+
}
112+
offset /= 60
113+
if offset < math.MinInt16 || offset > math.MaxInt16 {
114+
return nil, fmt.Errorf("Offset must be between %d and %d", math.MinInt16, math.MaxInt16)
115+
}
116+
94117
dt := new(Datetime)
95118
dt.time = t
96119
return dt, nil
@@ -110,8 +133,12 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
110133
var dt datetime
111134
dt.seconds = tm.Unix()
112135
dt.nsec = int32(tm.Nanosecond())
113-
dt.tzIndex = 0 // It is not implemented, see gh-163.
114-
dt.tzOffset = 0 // It is not implemented, see gh-163.
136+
137+
zone, offset := tm.Zone()
138+
if zone != noTimezoneZone {
139+
dt.tzIndex = int16(timezoneToIndex[zone])
140+
}
141+
dt.tzOffset = int16(offset / 60)
115142

116143
var bytesSize = secondsSize
117144
if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 {
@@ -132,7 +159,7 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
132159
func (tm *Datetime) UnmarshalMsgpack(b []byte) error {
133160
l := len(b)
134161
if l != maxSize && l != secondsSize {
135-
return fmt.Errorf("invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize)
162+
return fmt.Errorf("Invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize)
136163
}
137164

138165
var dt datetime
@@ -145,7 +172,23 @@ func (tm *Datetime) UnmarshalMsgpack(b []byte) error {
145172
dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:]))
146173
}
147174

148-
tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
175+
tt := time.Unix(dt.seconds, int64(dt.nsec))
176+
177+
loc := NoTimezone
178+
if dt.tzIndex != 0 || dt.tzOffset != 0 {
179+
zone := noTimezoneZone
180+
offset := int(dt.tzOffset) * 60
181+
182+
if dt.tzIndex != 0 {
183+
if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok {
184+
return fmt.Errorf("Unknown timezone index %d", dt.tzIndex)
185+
}
186+
zone = indexToTimezone[int(dt.tzIndex)]
187+
}
188+
loc = time.FixedZone(zone, offset)
189+
}
190+
tt = tt.In(loc)
191+
149192
dtp, err := NewDatetime(tt)
150193
if dtp != nil {
151194
*tm = *dtp

datetime/datetime_test.go

+177-56
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/hex"
55
"fmt"
66
"log"
7+
"math"
78
"os"
89
"reflect"
910
"testing"
@@ -74,6 +75,115 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) {
7475
}
7576
}
7677

78+
func TestTimezonesIndexMapping(t *testing.T) {
79+
for _, index := range TimezoneToIndex {
80+
if _, ok := IndexToTimezone[index]; !ok {
81+
t.Errorf("index %d not found", index)
82+
}
83+
}
84+
}
85+
86+
func TestTimezonesZonesMapping(t *testing.T) {
87+
for _, zone := range IndexToTimezone {
88+
if _, ok := TimezoneToIndex[zone]; !ok {
89+
t.Errorf("zone %s not found", zone)
90+
}
91+
}
92+
}
93+
94+
func TestInvalidTimezone(t *testing.T) {
95+
invalidLoc := time.FixedZone("AnyInvalid", 0)
96+
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:39:14Z")
97+
if err != nil {
98+
t.Fatalf("Time parse failed: %s", err)
99+
}
100+
tm = tm.In(invalidLoc)
101+
dt, err := NewDatetime(tm)
102+
if err == nil {
103+
t.Fatalf("Unexpected success: %v", dt)
104+
}
105+
}
106+
107+
func TestInvalidOffset(t *testing.T) {
108+
tests := []struct {
109+
ok bool
110+
offset int
111+
}{
112+
{ok: true, offset: math.MinInt16},
113+
{ok: true, offset: (math.MinInt16 + 1) * 60},
114+
{ok: true, offset: (math.MaxInt16 - 1) * 60},
115+
{ok: true, offset: math.MaxInt16},
116+
{ok: false, offset: (math.MinInt16 - 1) * 60},
117+
{ok: false, offset: (math.MaxInt16 + 1) * 60},
118+
}
119+
120+
for _, testcase := range tests {
121+
name := ""
122+
if testcase.ok {
123+
name = fmt.Sprintf("in_boundary_%d", testcase.offset)
124+
} else {
125+
name = fmt.Sprintf("out_of_boundary_%d", testcase.offset)
126+
}
127+
t.Run(name, func(t *testing.T) {
128+
loc := time.FixedZone("MSK", testcase.offset)
129+
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:39:14Z")
130+
if err != nil {
131+
t.Fatalf("Time parse failed: %s", err)
132+
}
133+
tm = tm.In(loc)
134+
dt, err := NewDatetime(tm)
135+
if testcase.ok && err != nil {
136+
t.Fatalf("Unexpected error: %s", err.Error())
137+
}
138+
if !testcase.ok && err == nil {
139+
t.Fatalf("Unexpected success: %v", dt)
140+
}
141+
})
142+
}
143+
}
144+
145+
func TestCustomTimezone(t *testing.T) {
146+
skipIfDatetimeUnsupported(t)
147+
148+
conn := test_helpers.ConnectWithValidation(t, server, opts)
149+
defer conn.Close()
150+
151+
customZone := "Europe/Moscow"
152+
customOffset := 180 * 60
153+
154+
customLoc := time.FixedZone(customZone, customOffset)
155+
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:44:14Z")
156+
if err != nil {
157+
t.Fatalf("Time parse failed: %s", err)
158+
}
159+
tm = tm.In(customLoc)
160+
dt, err := NewDatetime(tm)
161+
if err != nil {
162+
t.Fatalf("Unable to create datetime: %s", err.Error())
163+
}
164+
165+
resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"})
166+
if err != nil {
167+
t.Fatalf("Datetime replace failed %s", err.Error())
168+
}
169+
assertDatetimeIsEqual(t, resp.Data, tm)
170+
171+
tpl := resp.Data[0].([]interface{})
172+
respDt := tpl[0].(Datetime)
173+
zone, offset := respDt.ToTime().Zone()
174+
if zone != customZone {
175+
t.Fatalf("Expected zone %s instead of %s", customZone, zone)
176+
}
177+
if offset != customOffset {
178+
t.Fatalf("Expected offset %d instead of %d", customOffset, offset)
179+
}
180+
181+
_, err = conn.Delete(spaceTuple1, 0, []interface{}{dt})
182+
if err != nil {
183+
t.Fatalf("Datetime delete failed: %s", err.Error())
184+
}
185+
}
186+
77187
func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) {
78188
t.Helper()
79189

@@ -112,61 +222,63 @@ func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) {
112222
}
113223

114224
var datetimeSample = []struct {
225+
fmt string
115226
dt string
116227
mpBuf string // MessagePack buffer.
117228
}{
118-
{"2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"},
119-
{"1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"},
120-
{"2010-08-12T11:39:14Z", "d70462dd634c00000000"},
121-
{"1984-03-24T18:04:05Z", "d7041530c31a00000000"},
122-
{"2010-01-12T00:00:00Z", "d70480bb4b4b00000000"},
123-
{"1970-01-01T00:00:00Z", "d7040000000000000000"},
124-
{"1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"},
125-
{"1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"},
126-
{"1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"},
127-
{"1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"},
128-
{"1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"},
129-
{"1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"},
130-
{"1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"},
131-
{"1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"},
132-
{"1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"},
133-
{"1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"},
134-
{"1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"},
135-
{"1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"},
136-
{"1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"},
137-
{"1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"},
138-
{"1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"},
139-
{"1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"},
140-
{"1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"},
141-
{"1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"},
142-
{"1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"},
143-
{"1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"},
144-
{"1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"},
145-
{"1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"},
146-
{"1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"},
147-
{"1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"},
148-
{"1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"},
149-
{"1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"},
150-
{"1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"},
151-
{"1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"},
152-
{"1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"},
153-
{"1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"},
154-
{"1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"},
155-
{"1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"},
156-
{"1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"},
157-
{"1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"},
158-
{"1970-01-01T00:00:00.0Z", "d7040000000000000000"},
159-
{"1970-01-01T00:00:00.00Z", "d7040000000000000000"},
160-
{"1970-01-01T00:00:00.000Z", "d7040000000000000000"},
161-
{"1970-01-01T00:00:00.0000Z", "d7040000000000000000"},
162-
{"1970-01-01T00:00:00.00000Z", "d7040000000000000000"},
163-
{"1970-01-01T00:00:00.000000Z", "d7040000000000000000"},
164-
{"1970-01-01T00:00:00.0000000Z", "d7040000000000000000"},
165-
{"1970-01-01T00:00:00.00000000Z", "d7040000000000000000"},
166-
{"1970-01-01T00:00:00.000000000Z", "d7040000000000000000"},
167-
{"1973-11-29T21:33:09Z", "d70415cd5b0700000000"},
168-
{"2013-10-28T17:51:56Z", "d7043ca46e5200000000"},
169-
{"9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"},
229+
{time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"},
230+
{time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"},
231+
{time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000"},
232+
{time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000"},
233+
{time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000"},
234+
{time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000"},
235+
{time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"},
236+
{time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"},
237+
{time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"},
238+
{time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"},
239+
{time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"},
240+
{time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"},
241+
{time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"},
242+
{time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"},
243+
{time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"},
244+
{time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"},
245+
{time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"},
246+
{time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"},
247+
{time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"},
248+
{time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"},
249+
{time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"},
250+
{time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"},
251+
{time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"},
252+
{time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"},
253+
{time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"},
254+
{time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"},
255+
{time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"},
256+
{time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"},
257+
{time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"},
258+
{time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"},
259+
{time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"},
260+
{time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"},
261+
{time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"},
262+
{time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"},
263+
{time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"},
264+
{time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"},
265+
{time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"},
266+
{time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"},
267+
{time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"},
268+
{time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"},
269+
{time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000"},
270+
{time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000"},
271+
{time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000"},
272+
{time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000"},
273+
{time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000"},
274+
{time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000"},
275+
{time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000"},
276+
{time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000"},
277+
{time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000"},
278+
{time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000"},
279+
{time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000"},
280+
{time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"},
281+
{time.RFC3339 + " MST", "2006-01-02T15:04:00+03:00 MSK", "d804b016b9430000000000000000b400ee00"},
170282
}
171283

172284
func TestDatetimeInsertSelectDelete(t *testing.T) {
@@ -177,7 +289,10 @@ func TestDatetimeInsertSelectDelete(t *testing.T) {
177289

178290
for _, testcase := range datetimeSample {
179291
t.Run(testcase.dt, func(t *testing.T) {
180-
tm, err := time.Parse(time.RFC3339, testcase.dt)
292+
tm, err := time.Parse(testcase.fmt, testcase.dt)
293+
if testcase.fmt == time.RFC3339 {
294+
tm = tm.In(NoTimezone)
295+
}
181296
if err != nil {
182297
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
183298
}
@@ -517,7 +632,10 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) {
517632
func TestMPEncode(t *testing.T) {
518633
for _, testcase := range datetimeSample {
519634
t.Run(testcase.dt, func(t *testing.T) {
520-
tm, err := time.Parse(time.RFC3339, testcase.dt)
635+
tm, err := time.Parse(testcase.fmt, testcase.dt)
636+
if testcase.fmt == time.RFC3339 {
637+
tm = tm.In(NoTimezone)
638+
}
521639
if err != nil {
522640
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
523641
}
@@ -531,7 +649,7 @@ func TestMPEncode(t *testing.T) {
531649
}
532650
refBuf, _ := hex.DecodeString(testcase.mpBuf)
533651
if reflect.DeepEqual(buf, refBuf) != true {
534-
t.Fatalf("Failed to encode datetime '%s', actual %v, expected %v",
652+
t.Fatalf("Failed to encode datetime '%s', actual %x, expected %x",
535653
testcase.dt,
536654
buf,
537655
refBuf)
@@ -543,7 +661,10 @@ func TestMPEncode(t *testing.T) {
543661
func TestMPDecode(t *testing.T) {
544662
for _, testcase := range datetimeSample {
545663
t.Run(testcase.dt, func(t *testing.T) {
546-
tm, err := time.Parse(time.RFC3339, testcase.dt)
664+
tm, err := time.Parse(testcase.fmt, testcase.dt)
665+
if testcase.fmt == time.RFC3339 {
666+
tm = tm.In(NoTimezone)
667+
}
547668
if err != nil {
548669
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
549670
}

0 commit comments

Comments
 (0)