Skip to content

Commit c4d60c6

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 c4d60c6

File tree

4 files changed

+355
-1
lines changed

4 files changed

+355
-1
lines changed

README.md

+3-1
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

+44
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

+96
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

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

0 commit comments

Comments
 (0)