Skip to content

Commit

Permalink
datetime: add TZ support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
oleg-jukovec committed Jul 28, 2022
1 parent 609268f commit 98b5f01
Show file tree
Hide file tree
Showing 8 changed files with 1,793 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
- Support datetime type in msgpack (#118)
- Prepared SQL statements (#117)
- Context support for request objects (#48)
- TZ support for datetime (#163)

### Changed

Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ clean:
deps: clean
( cd ./queue; tarantoolctl rocks install queue 1.1.0 )

.PHONY: datetime-timezones
datetime-timezones:
(cd ./datetime; ./gen-timezones.sh)

.PHONY: format
format:
goimports -l -w .
Expand Down
51 changes: 47 additions & 4 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package datetime
import (
"encoding/binary"
"fmt"
"math"
"time"

"gopkg.in/vmihailenco/msgpack.v2"
Expand Down Expand Up @@ -81,6 +82,16 @@ type Datetime struct {
time time.Time
}

const (
noTimezoneZone = ""
noTimezoneOffset = 0
)

// NoTimezone allows to create a datetime without UTC timezone for
// Tarantool. The problem is that Golang by default creates a time value with
// UTC timezone. So it is a way to create a datetime without timezone.
var NoTimezone = time.FixedZone(noTimezoneZone, noTimezoneOffset)

// NewDatetime returns a pointer to a new datetime.Datetime that contains a
// specified time.Time. It may returns an error if the Time value is out of
// supported range: [-5879610-06-22T00:00Z .. 5879611-07-11T00:00Z]
Expand All @@ -91,6 +102,18 @@ func NewDatetime(t time.Time) (*Datetime, error) {
return nil, fmt.Errorf("Time %s is out of supported range.", t)
}

zone, offset := t.Zone()
if zone != noTimezoneZone {
if _, ok := timezoneToIndex[zone]; !ok {
return nil, fmt.Errorf("Unknown timezone %s with offset %d",
zone, offset)
}
}
offset /= 60
if offset < math.MinInt16 || offset > math.MaxInt16 {
return nil, fmt.Errorf("Offset must be between %d and %d", math.MinInt16, math.MaxInt16)
}

dt := new(Datetime)
dt.time = t
return dt, nil
Expand All @@ -110,8 +133,12 @@ func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
var dt datetime
dt.seconds = tm.Unix()
dt.nsec = int32(tm.Nanosecond())
dt.tzIndex = 0 // It is not implemented, see gh-163.
dt.tzOffset = 0 // It is not implemented, see gh-163.

zone, offset := tm.Zone()
if zone != noTimezoneZone {
dt.tzIndex = int16(timezoneToIndex[zone])
}
dt.tzOffset = int16(offset / 60)

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

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

tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
tt := time.Unix(dt.seconds, int64(dt.nsec))

loc := NoTimezone
if dt.tzIndex != 0 || dt.tzOffset != 0 {
zone := noTimezoneZone
offset := int(dt.tzOffset) * 60

if dt.tzIndex != 0 {
if _, ok := indexToTimezone[int(dt.tzIndex)]; !ok {
return fmt.Errorf("Unknown timezone index %d", dt.tzIndex)
}
zone = indexToTimezone[int(dt.tzIndex)]
}
loc = time.FixedZone(zone, offset)
}
tt = tt.In(loc)

dtp, err := NewDatetime(tt)
if dtp != nil {
*tm = *dtp
Expand Down
233 changes: 177 additions & 56 deletions datetime/datetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"fmt"
"log"
"math"
"os"
"reflect"
"testing"
Expand Down Expand Up @@ -74,6 +75,115 @@ func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) {
}
}

func TestTimezonesIndexMapping(t *testing.T) {
for _, index := range TimezoneToIndex {
if _, ok := IndexToTimezone[index]; !ok {
t.Errorf("index %d not found", index)
}
}
}

func TestTimezonesZonesMapping(t *testing.T) {
for _, zone := range IndexToTimezone {
if _, ok := TimezoneToIndex[zone]; !ok {
t.Errorf("zone %s not found", zone)
}
}
}

func TestInvalidTimezone(t *testing.T) {
invalidLoc := time.FixedZone("AnyInvalid", 0)
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:39:14Z")
if err != nil {
t.Fatalf("Time parse failed: %s", err)
}
tm = tm.In(invalidLoc)
dt, err := NewDatetime(tm)
if err == nil {
t.Fatalf("Unexpected success: %v", dt)
}
}

func TestInvalidOffset(t *testing.T) {
tests := []struct {
ok bool
offset int
}{
{ok: true, offset: math.MinInt16},
{ok: true, offset: (math.MinInt16 + 1) * 60},
{ok: true, offset: (math.MaxInt16 - 1) * 60},
{ok: true, offset: math.MaxInt16},
{ok: false, offset: (math.MinInt16 - 1) * 60},
{ok: false, offset: (math.MaxInt16 + 1) * 60},
}

for _, testcase := range tests {
name := ""
if testcase.ok {
name = fmt.Sprintf("in_boundary_%d", testcase.offset)
} else {
name = fmt.Sprintf("out_of_boundary_%d", testcase.offset)
}
t.Run(name, func(t *testing.T) {
loc := time.FixedZone("MSK", testcase.offset)
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:39:14Z")
if err != nil {
t.Fatalf("Time parse failed: %s", err)
}
tm = tm.In(loc)
dt, err := NewDatetime(tm)
if testcase.ok && err != nil {
t.Fatalf("Unexpected error: %s", err.Error())
}
if !testcase.ok && err == nil {
t.Fatalf("Unexpected success: %v", dt)
}
})
}
}

func TestCustomTimezone(t *testing.T) {
skipIfDatetimeUnsupported(t)

conn := test_helpers.ConnectWithValidation(t, server, opts)
defer conn.Close()

customZone := "Europe/Moscow"
customOffset := 180 * 60

customLoc := time.FixedZone(customZone, customOffset)
tm, err := time.Parse(time.RFC3339, "2010-08-12T11:44:14Z")
if err != nil {
t.Fatalf("Time parse failed: %s", err)
}
tm = tm.In(customLoc)
dt, err := NewDatetime(tm)
if err != nil {
t.Fatalf("Unable to create datetime: %s", err.Error())
}

resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"})
if err != nil {
t.Fatalf("Datetime replace failed %s", err.Error())
}
assertDatetimeIsEqual(t, resp.Data, tm)

tpl := resp.Data[0].([]interface{})
respDt := tpl[0].(Datetime)
zone, offset := respDt.ToTime().Zone()
if zone != customZone {
t.Fatalf("Expected zone %s instead of %s", customZone, zone)
}
if offset != customOffset {
t.Fatalf("Expected offset %d instead of %d", customOffset, offset)
}

_, err = conn.Delete(spaceTuple1, 0, []interface{}{dt})
if err != nil {
t.Fatalf("Datetime delete failed: %s", err.Error())
}
}

func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) {
t.Helper()

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

var datetimeSample = []struct {
fmt string
dt string
mpBuf string // MessagePack buffer.
}{
{"2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"},
{"1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"},
{"2010-08-12T11:39:14Z", "d70462dd634c00000000"},
{"1984-03-24T18:04:05Z", "d7041530c31a00000000"},
{"2010-01-12T00:00:00Z", "d70480bb4b4b00000000"},
{"1970-01-01T00:00:00Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"},
{"1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"},
{"1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"},
{"1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"},
{"1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"},
{"1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"},
{"1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"},
{"1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"},
{"1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"},
{"1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"},
{"1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"},
{"1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"},
{"1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"},
{"1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"},
{"1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"},
{"1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"},
{"1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"},
{"1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"},
{"1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"},
{"1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"},
{"1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"},
{"1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"},
{"1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"},
{"1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"},
{"1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"},
{"1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"},
{"1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"},
{"1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"},
{"1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"},
{"1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"},
{"1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"},
{"1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"},
{"1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"},
{"1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"},
{"1970-01-01T00:00:00.0Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.00Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.000Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.0000Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.00000Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.000000Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.0000000Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.00000000Z", "d7040000000000000000"},
{"1970-01-01T00:00:00.000000000Z", "d7040000000000000000"},
{"1973-11-29T21:33:09Z", "d70415cd5b0700000000"},
{"2013-10-28T17:51:56Z", "d7043ca46e5200000000"},
{"9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"},
{time.RFC3339, "2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"},
{time.RFC3339, "2010-08-12T11:39:14Z", "d70462dd634c00000000"},
{time.RFC3339, "1984-03-24T18:04:05Z", "d7041530c31a00000000"},
{time.RFC3339, "2010-01-12T00:00:00Z", "d70480bb4b4b00000000"},
{time.RFC3339, "1970-01-01T00:00:00Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"},
{time.RFC3339, "1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"},
{time.RFC3339, "1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"},
{time.RFC3339, "1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"},
{time.RFC3339, "1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"},
{time.RFC3339, "1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"},
{time.RFC3339, "1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"},
{time.RFC3339, "1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"},
{time.RFC3339, "1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"},
{time.RFC3339, "1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"},
{time.RFC3339, "1970-01-01T00:00:00.0Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.0000000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.00000000Z", "d7040000000000000000"},
{time.RFC3339, "1970-01-01T00:00:00.000000000Z", "d7040000000000000000"},
{time.RFC3339, "1973-11-29T21:33:09Z", "d70415cd5b0700000000"},
{time.RFC3339, "2013-10-28T17:51:56Z", "d7043ca46e5200000000"},
{time.RFC3339, "9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"},
{time.RFC822, "02 Jan 06 15:04 MSK", "d804b016b9430000000000000000b400ee00"},
}

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

for _, testcase := range datetimeSample {
t.Run(testcase.dt, func(t *testing.T) {
tm, err := time.Parse(time.RFC3339, testcase.dt)
tm, err := time.Parse(testcase.fmt, testcase.dt)
if testcase.fmt == time.RFC3339 {
tm = tm.In(NoTimezone)
}
if err != nil {
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
}
Expand Down Expand Up @@ -517,7 +632,10 @@ func TestCustomEncodeDecodeTuple5(t *testing.T) {
func TestMPEncode(t *testing.T) {
for _, testcase := range datetimeSample {
t.Run(testcase.dt, func(t *testing.T) {
tm, err := time.Parse(time.RFC3339, testcase.dt)
tm, err := time.Parse(testcase.fmt, testcase.dt)
if testcase.fmt == time.RFC3339 {
tm = tm.In(NoTimezone)
}
if err != nil {
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
}
Expand All @@ -531,7 +649,7 @@ func TestMPEncode(t *testing.T) {
}
refBuf, _ := hex.DecodeString(testcase.mpBuf)
if reflect.DeepEqual(buf, refBuf) != true {
t.Fatalf("Failed to encode datetime '%s', actual %v, expected %v",
t.Fatalf("Failed to encode datetime '%s', actual %x, expected %x",
testcase.dt,
buf,
refBuf)
Expand All @@ -543,7 +661,10 @@ func TestMPEncode(t *testing.T) {
func TestMPDecode(t *testing.T) {
for _, testcase := range datetimeSample {
t.Run(testcase.dt, func(t *testing.T) {
tm, err := time.Parse(time.RFC3339, testcase.dt)
tm, err := time.Parse(testcase.fmt, testcase.dt)
if testcase.fmt == time.RFC3339 {
tm = tm.In(NoTimezone)
}
if err != nil {
t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err)
}
Expand Down
Loading

0 comments on commit 98b5f01

Please sign in to comment.