Skip to content

Commit 41c5936

Browse files
committed
datetime: add datetime type in msgpack
This patch provides datetime support for all space operations and as function return result. Datetime type was introduced in Tarantool 2.10. See more in issue [1]. 1. tarantool/tarantool#5946 Closes #118
1 parent d3b5696 commit 41c5936

File tree

4 files changed

+336
-1
lines changed

4 files changed

+336
-1
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,9 +698,11 @@ and call
698698
```bash
699699
go clean -testcache && go test -v
700700
```
701-
Use the same for main `tarantool` package and `queue` and `uuid` subpackages.
701+
Use the same for main `tarantool` package, `queue`, `uuid` and `datetime` subpackages.
702702
`uuid` tests require
703703
[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5).
704+
`datetime` tests require
705+
[Tarantool 2.10 or newer](https://github.com/tarantool/tarantool/issues/5946).
704706

705707
## Alternative connectors
706708

datetime/config.lua

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
local datetime = require('datetime')
2+
local msgpack = require('msgpack')
3+
4+
-- Do not set listen for now so connector won't be
5+
-- able to send requests until everything is configured.
6+
box.cfg{
7+
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
8+
}
9+
10+
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
11+
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
12+
13+
local datetime_msgpack_supported = pcall(msgpack.encode, datetime.new())
14+
if not datetime_msgpack_supported then
15+
error('Datetime unsupported, use Tarantool 2.10 or newer')
16+
end
17+
18+
local s = box.schema.space.create('testDatetime', {
19+
id = 524,
20+
if_not_exists = true,
21+
})
22+
s:create_index('primary', {
23+
type = 'TREE',
24+
parts = {
25+
{
26+
field = 1,
27+
type = 'datetime',
28+
},
29+
},
30+
if_not_exists = true
31+
})
32+
s:truncate()
33+
34+
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime', { if_not_exists = true })
35+
box.schema.user.grant('guest', 'read,write', 'space', 'testDatetime', { if_not_exists = true })
36+
37+
s:insert({ datetime.new() })
38+
39+
-- Set listen only when every other thing is configured.
40+
box.cfg{
41+
listen = os.getenv("TEST_TNT_LISTEN"),
42+
}
43+
44+
require('console').start()

datetime/datetime.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package datetime
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"encoding/binary"
8+
9+
"gopkg.in/vmihailenco/msgpack.v2"
10+
)
11+
12+
// Datetime external type
13+
// Supported since Tarantool 2.10. See more details in issue
14+
// https://github.com/tarantool/tarantool/issues/5946
15+
16+
const Datetime_extId = 4
17+
18+
const (
19+
SEC_LEN = 8
20+
NSEC_LEN = 4
21+
TZ_OFFSET_LEN = 2
22+
TZ_INDEX_LEN = 2
23+
)
24+
25+
/**
26+
* datetime structure keeps number of seconds and
27+
* nanoseconds since Unix Epoch.
28+
* Time is normalized by UTC, so time-zone offset
29+
* is informative only.
30+
*/
31+
type EventTime struct {
32+
// Seconds since Epoch
33+
Seconds int64
34+
// Nanoseconds, if any
35+
Nsec int32
36+
// Offset in minutes from UTC
37+
TZOffset int16
38+
// Olson timezone id
39+
TZIndex int16
40+
}
41+
42+
func encodeDatetime(e *msgpack.Encoder, v reflect.Value) error {
43+
tm := v.Interface().(EventTime)
44+
45+
var payloadLen = 8
46+
if tm.Nsec != 0 || tm.TZOffset != 0 || tm.TZIndex != 0 {
47+
payloadLen = 16
48+
}
49+
50+
b := make([]byte, payloadLen)
51+
binary.LittleEndian.PutUint64(b, uint64(tm.Seconds))
52+
53+
if payloadLen == 16 {
54+
binary.LittleEndian.PutUint32(b[NSEC_LEN:], uint32(tm.Nsec))
55+
binary.LittleEndian.PutUint16(b[TZ_OFFSET_LEN:], uint16(tm.TZOffset))
56+
binary.LittleEndian.PutUint16(b[TZ_INDEX_LEN:], uint16(tm.TZIndex))
57+
}
58+
59+
_, err := e.Writer().Write(b)
60+
if err != nil {
61+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
62+
}
63+
64+
return nil
65+
}
66+
67+
func decodeDatetime(d *msgpack.Decoder, v reflect.Value) error {
68+
var tm EventTime
69+
var err error
70+
71+
tm.Seconds, err = d.DecodeInt64()
72+
if err != nil {
73+
return fmt.Errorf("msgpack: can't read bytes on datetime seconds decode: %w", err)
74+
}
75+
76+
tm.Nsec, err = d.DecodeInt32()
77+
if err == nil {
78+
tm.TZOffset, err = d.DecodeInt16()
79+
if err != nil {
80+
return fmt.Errorf("msgpack: can't read bytes on datetime tzoffset decode: %w", err)
81+
}
82+
tm.TZIndex, err = d.DecodeInt16()
83+
if err != nil {
84+
return fmt.Errorf("msgpack: can't read bytes on datetime tzindex decode: %w", err)
85+
}
86+
}
87+
88+
v.Set(reflect.ValueOf(tm))
89+
90+
return nil
91+
}
92+
93+
func init() {
94+
msgpack.Register(reflect.TypeOf((*EventTime)(nil)).Elem(), encodeDatetime, decodeDatetime)
95+
msgpack.RegisterExt(Datetime_extId, (*EventTime)(nil))
96+
}

datetime/datetime_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package datetime_test
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"testing"
8+
"time"
9+
10+
. "github.com/tarantool/go-tarantool"
11+
"github.com/tarantool/go-tarantool/datetime"
12+
"github.com/tarantool/go-tarantool/test_helpers"
13+
"gopkg.in/vmihailenco/msgpack.v2"
14+
)
15+
16+
// There is no way to skip tests in testing.M,
17+
// so we use this variable to pass info
18+
// to each testing.T that it should skip.
19+
var isDatetimeSupported = false
20+
21+
var server = "127.0.0.1:3013"
22+
var opts = Opts{
23+
Timeout: 500 * time.Millisecond,
24+
User: "test",
25+
Pass: "test",
26+
}
27+
28+
var space = "testDatetime"
29+
var index = "primary"
30+
31+
type TupleDatetime struct {
32+
tm datetime.EventTime
33+
}
34+
35+
func (t *TupleDatetime) DecodeMsgpack(d *msgpack.Decoder) error {
36+
var err error
37+
var l int
38+
if l, err = d.DecodeSliceLen(); err != nil {
39+
return err
40+
}
41+
if l != 1 {
42+
return fmt.Errorf("array len doesn't match: %d", l)
43+
}
44+
45+
res, err := d.DecodeInterface()
46+
if err != nil {
47+
return err
48+
}
49+
t.tm = res.(datetime.EventTime)
50+
51+
return nil
52+
}
53+
54+
func connectWithValidation(t *testing.T) *Connection {
55+
conn, err := Connect(server, opts)
56+
if err != nil {
57+
t.Errorf("Failed to connect: %s", err.Error())
58+
}
59+
if conn == nil {
60+
t.Errorf("conn is nil after Connect")
61+
}
62+
return conn
63+
}
64+
65+
func tupleValueIsDatetime(t *testing.T, tuples []interface{}, tm datetime.EventTime) {
66+
if tpl, ok := tuples[0].([]interface{}); !ok {
67+
t.Errorf("Unexpected return value body")
68+
} else {
69+
if len(tpl) != 1 {
70+
t.Errorf("Unexpected return value body (tuple len)")
71+
}
72+
if val, ok := tpl[0].(datetime.EventTime); !ok || val != tm {
73+
t.Errorf("Unexpected return value body (tuple 0 field)")
74+
}
75+
}
76+
}
77+
78+
func BytesToString(data []byte) string {
79+
return string(data[:])
80+
}
81+
82+
func TestSelect(t *testing.T) {
83+
if isDatetimeSupported == false {
84+
t.Skip("Skipping test for Tarantool without decimanl support in msgpack")
85+
}
86+
87+
conn := connectWithValidation(t)
88+
defer conn.Close()
89+
90+
tm := datetime.EventTime{0, 0, 0, 0}
91+
92+
var offset uint32 = 0
93+
var limit uint32 = 1
94+
resp, errSel := conn.Select(space, index, offset, limit, IterEq, []interface{}{tm})
95+
if errSel != nil {
96+
t.Errorf("Datetime select failed: %s", errSel.Error())
97+
}
98+
if resp == nil {
99+
t.Errorf("Response is nil after Select")
100+
}
101+
tupleValueIsDatetime(t, resp.Data, tm)
102+
103+
var tuples []TupleDatetime
104+
errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{tm}, &tuples)
105+
if errTyp != nil {
106+
t.Errorf("Failed to SelectTyped: %s", errTyp.Error())
107+
}
108+
if len(tuples) != 1 {
109+
t.Errorf("Result len of SelectTyped != 1")
110+
}
111+
if tuples[0].tm != tm {
112+
t.Errorf("Bad value loaded from SelectTyped: %d", tuples[0].tm.Seconds)
113+
}
114+
}
115+
116+
func TestReplace(t *testing.T) {
117+
t.Skip("Not imeplemented")
118+
119+
if isDatetimeSupported == false {
120+
t.Skip("Skipping test for Tarantool without decimal support in msgpack")
121+
}
122+
123+
conn := connectWithValidation(t)
124+
defer conn.Close()
125+
126+
/*
127+
number, err := decimal.NewFromString("-12.34")
128+
if err != nil {
129+
t.Errorf("Failed to prepare test decimal: %s", err)
130+
}
131+
132+
respRep, errRep := conn.Replace(space, []interface{}{number})
133+
if errRep != nil {
134+
t.Errorf("Decimal replace failed: %s", errRep)
135+
}
136+
if respRep == nil {
137+
t.Errorf("Response is nil after Replace")
138+
}
139+
tupleValueIsDatetime(t, respRep.Data, number)
140+
141+
respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{number})
142+
if errSel != nil {
143+
t.Errorf("Decimal select failed: %s", errSel)
144+
}
145+
if respSel == nil {
146+
t.Errorf("Response is nil after Select")
147+
}
148+
tupleValueIsDatetime(t, respSel.Data, number)
149+
*/
150+
}
151+
152+
// runTestMain is a body of TestMain function
153+
// (see https://pkg.go.dev/testing#hdr-Main).
154+
// Using defer + os.Exit is not works so TestMain body
155+
// is a separate function, see
156+
// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls
157+
func runTestMain(m *testing.M) int {
158+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 2, 0)
159+
if err != nil {
160+
log.Fatalf("Failed to extract Tarantool version: %s", err)
161+
}
162+
163+
if isLess {
164+
log.Println("Skipping datetime tests...")
165+
isDatetimeSupported = false
166+
return m.Run()
167+
} else {
168+
isDatetimeSupported = true
169+
}
170+
171+
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
172+
InitScript: "config.lua",
173+
Listen: server,
174+
WorkDir: "work_dir",
175+
User: opts.User,
176+
Pass: opts.Pass,
177+
WaitStart: 100 * time.Millisecond,
178+
ConnectRetry: 3,
179+
RetryTimeout: 500 * time.Millisecond,
180+
})
181+
defer test_helpers.StopTarantoolWithCleanup(instance)
182+
183+
if err != nil {
184+
log.Fatalf("Failed to prepare test Tarantool: %s", err)
185+
}
186+
187+
return m.Run()
188+
}
189+
190+
func TestMain(m *testing.M) {
191+
code := runTestMain(m)
192+
os.Exit(code)
193+
}

0 commit comments

Comments
 (0)