Skip to content

Commit fa131df

Browse files
committed
Support 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 bec9f72 commit fa131df

File tree

4 files changed

+336
-1
lines changed

4 files changed

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

+193
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)