Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f60d0c4

Browse files
committedMar 16, 2022
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]. Note that timezone's index and offset are not implemented in Tarantool, see [2]. This Lua snippet was quite useful for debugging encoding and decoding datetime in MessagePack: ``` local msgpack = require('msgpack') local datetime = require('datetime') local dt = datetime.parse('2012-01-31T23:59:59.000000010Z') local mp_dt = msgpack.encode(dt):gsub('.', function (c) return string.format('%02x', string.byte(c)) end) print(dt, mp_dt) ``` 1. tarantool/tarantool#5946 2. tarantool/tarantool#6751 Closes #118
1 parent d3b5696 commit f60d0c4

File tree

4 files changed

+479
-1
lines changed

4 files changed

+479
-1
lines changed
 

‎README.md

+46-1
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,49 @@ func main() {
329329
}
330330
```
331331

332+
To enable support of datetime in msgpack with builtin module [time](https://pkg.go.dev/time),
333+
import `tarantool/datimetime` submodule.
334+
```go
335+
package main
336+
337+
import (
338+
"log"
339+
"time"
340+
341+
"github.com/tarantool/go-tarantool"
342+
_ "github.com/tarantool/go-tarantool/datetime"
343+
)
344+
345+
func main() {
346+
server := "127.0.0.1:3013"
347+
opts := tarantool.Opts{
348+
Timeout: 500 * time.Millisecond,
349+
Reconnect: 1 * time.Second,
350+
MaxReconnects: 3,
351+
User: "test",
352+
Pass: "test",
353+
}
354+
client, err := tarantool.Connect(server, opts)
355+
if err != nil {
356+
log.Fatalf("Failed to connect: %s", err.Error())
357+
}
358+
defer client.Close()
359+
360+
spaceNo := uint32(524)
361+
362+
tm, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
363+
if err != nil {
364+
log.Fatalf("Failed to parse time: %s", err)
365+
}
366+
367+
resp, err := client.Insert(spaceNo, []interface{}{tm})
368+
369+
log.Println("Error:", err)
370+
log.Println("Code:", resp.Code)
371+
log.Println("Data:", resp.Data)
372+
}
373+
```
374+
332375
## Schema
333376

334377
```go
@@ -698,9 +741,11 @@ and call
698741
```bash
699742
go clean -testcache && go test -v
700743
```
701-
Use the same for main `tarantool` package and `queue` and `uuid` subpackages.
744+
Use the same for main `tarantool` package, `queue`, `uuid` and `datetime` subpackages.
702745
`uuid` tests require
703746
[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5).
747+
`datetime` tests require
748+
[Tarantool 2.10 or newer](https://github.com/tarantool/tarantool/issues/5946).
704749

705750
## Alternative connectors
706751

‎datetime/config.lua

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
-- Set listen only when every other thing is configured.
38+
box.cfg{
39+
listen = os.getenv("TEST_TNT_LISTEN"),
40+
}
41+
42+
require('console').start()

‎datetime/datetime.go

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Datetime MessagePack serialization schema is an MP_EXT extension, which
3+
* creates container of 8 or 16 bytes long payload.
4+
*
5+
* +---------+--------+===============+-------------------------------+
6+
* |0xd7/0xd8|type (4)| seconds (8b) | nsec; tzoffset; tzindex; (8b) |
7+
* +---------+--------+===============+-------------------------------+
8+
*
9+
* MessagePack data encoded using fixext8 (0xd7) or fixext16 (0xd8), and may
10+
* contain:
11+
*
12+
* - [required] seconds parts as full, unencoded, signed 64-bit integer, stored
13+
* in little-endian order;
14+
*
15+
* - [optional] all the other fields (nsec, tzoffset, tzindex) if any of them
16+
* were having not 0 value. They are packed naturally in little-endian order;
17+
*
18+
*/
19+
20+
package datetime
21+
22+
import (
23+
"fmt"
24+
"math"
25+
"reflect"
26+
"time"
27+
28+
"encoding/binary"
29+
30+
"gopkg.in/vmihailenco/msgpack.v2"
31+
)
32+
33+
// Datetime external type
34+
// Supported since Tarantool 2.10. See more details in issue
35+
// https://github.com/tarantool/tarantool/issues/5946
36+
const Datetime_extId = 4
37+
38+
/**
39+
* datetime structure keeps number of seconds and
40+
* nanoseconds since Unix Epoch.
41+
* Time is normalized by UTC, so time-zone offset
42+
* is informative only.
43+
*/
44+
type datetime struct {
45+
// Seconds since Epoch
46+
seconds int64
47+
// Nanoseconds, fractional part of seconds
48+
nsec int32
49+
// Timezone offset in minutes from UTC
50+
// (not implemented in Tarantool, https://github.com/tarantool/tarantool/issues/6751)
51+
tzOffset int16
52+
// Olson timezone id
53+
// (not implemented in Tarantool, https://github.com/tarantool/tarantool/issues/6751)
54+
tzIndex int16
55+
}
56+
57+
const (
58+
secondsSize = 8
59+
nsecSize = 4
60+
tzIndexSize = 2
61+
tzOffsetSize = 2
62+
)
63+
64+
func encodeDatetime(e *msgpack.Encoder, v reflect.Value) error {
65+
var dt datetime
66+
67+
tm := v.Interface().(time.Time)
68+
dt.seconds = tm.Unix()
69+
nsec := tm.Nanosecond()
70+
dt.nsec = int32(math.Round((10000 * float64(nsec)) / 10000))
71+
dt.tzIndex = 0 /* not implemented */
72+
dt.tzOffset = 0 /* not implemented */
73+
74+
var bytesSize = secondsSize
75+
if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 {
76+
bytesSize += nsecSize + tzIndexSize + tzOffsetSize
77+
}
78+
79+
buf := make([]byte, bytesSize)
80+
binary.LittleEndian.PutUint64(buf[0:], uint64(dt.seconds))
81+
if bytesSize == 16 {
82+
binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec))
83+
binary.LittleEndian.PutUint16(buf[nsecSize:], uint16(dt.tzOffset))
84+
binary.LittleEndian.PutUint16(buf[tzOffsetSize:], uint16(dt.tzIndex))
85+
}
86+
87+
_, err := e.Writer().Write(buf)
88+
if err != nil {
89+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
90+
}
91+
92+
return nil
93+
}
94+
95+
func decodeDatetime(d *msgpack.Decoder, v reflect.Value) error {
96+
var dt datetime
97+
secondsBytes := make([]byte, secondsSize)
98+
n, err := d.Buffered().Read(secondsBytes)
99+
if err != nil {
100+
return fmt.Errorf("msgpack: can't read bytes on datetime's seconds decode: %w", err)
101+
}
102+
if n < secondsSize {
103+
return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n)
104+
}
105+
dt.seconds = int64(binary.LittleEndian.Uint64(secondsBytes))
106+
dt.nsec = 0
107+
tailSize := nsecSize + tzOffsetSize + tzIndexSize
108+
tailBytes := make([]byte, tailSize)
109+
n, err = d.Buffered().Read(tailBytes)
110+
// Part with nanoseconds, tzoffset and tzindex is optional,
111+
// so we don't need to handle an error here.
112+
if err == nil {
113+
if n < tailSize {
114+
return fmt.Errorf("msgpack: can't read bytes on datetime's tail decode: %w", err)
115+
}
116+
dt.nsec = int32(binary.LittleEndian.Uint32(tailBytes[0:]))
117+
dt.tzOffset = int16(binary.LittleEndian.Uint16(tailBytes[nsecSize:]))
118+
dt.tzIndex = int16(binary.LittleEndian.Uint16(tailBytes[tzOffsetSize:]))
119+
}
120+
t := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
121+
v.Set(reflect.ValueOf(t))
122+
123+
return nil
124+
}
125+
126+
func init() {
127+
msgpack.Register(reflect.TypeOf((*time.Time)(nil)).Elem(), encodeDatetime, decodeDatetime)
128+
msgpack.RegisterExt(Datetime_extId, (*time.Time)(nil))
129+
}

‎datetime/datetime_test.go

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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/test_helpers"
12+
"gopkg.in/vmihailenco/msgpack.v2"
13+
)
14+
15+
// There is no way to skip tests in testing.M,
16+
// so we use this variable to pass info
17+
// to each testing.T that it should skip.
18+
var isDatetimeSupported = false
19+
20+
var server = "127.0.0.1:3013"
21+
var opts = Opts{
22+
Timeout: 500 * time.Millisecond,
23+
User: "test",
24+
Pass: "test",
25+
}
26+
27+
var space = "testDatetime"
28+
var index = "primary"
29+
30+
type TupleDatetime struct {
31+
tm time.Time
32+
}
33+
34+
func (t *TupleDatetime) DecodeMsgpack(d *msgpack.Decoder) error {
35+
var err error
36+
var l int
37+
if l, err = d.DecodeSliceLen(); err != nil {
38+
return err
39+
}
40+
if l != 1 {
41+
return fmt.Errorf("array len doesn't match: %d", l)
42+
}
43+
44+
res, err := d.DecodeInterface()
45+
if err != nil {
46+
return err
47+
}
48+
t.tm = res.(time.Time)
49+
50+
return nil
51+
}
52+
53+
func connectWithValidation(t *testing.T) *Connection {
54+
conn, err := Connect(server, opts)
55+
if err != nil {
56+
t.Errorf("Failed to connect: %s", err.Error())
57+
}
58+
if conn == nil {
59+
t.Errorf("conn is nil after Connect")
60+
}
61+
return conn
62+
}
63+
64+
func tupleValueIsDatetime(t *testing.T, tuples []interface{}, tm time.Time) {
65+
if tpl, ok := tuples[0].([]interface{}); !ok {
66+
t.Errorf("Unexpected return value body")
67+
} else {
68+
if len(tpl) != 1 {
69+
t.Errorf("Unexpected return value body (tuple len = %d)", len(tpl))
70+
}
71+
if val, ok := tpl[0].(time.Time); !ok || val != tm {
72+
fmt.Println("Tuple: ", tpl[0])
73+
fmt.Println("Expected:", val)
74+
t.Errorf("Unexpected return value body (tuple 0 field)")
75+
}
76+
}
77+
}
78+
79+
func tupleInsertSelectDelete(t *testing.T, conn *Connection, datetime string) {
80+
tm, err := time.Parse(time.RFC3339, datetime)
81+
if err != nil {
82+
t.Errorf("Time (%s) parse failed: %s", datetime, err)
83+
}
84+
85+
// Insert tuple with datetime.
86+
resp, err := conn.Insert(space, []interface{}{tm})
87+
if err != nil {
88+
t.Errorf("Datetime insert failed: %s", err.Error())
89+
}
90+
fmt.Println(resp)
91+
92+
// Select tuple with datetime.
93+
var offset uint32 = 0
94+
var limit uint32 = 1
95+
resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{tm})
96+
if err != nil {
97+
t.Errorf("Datetime select failed: %s", err.Error())
98+
}
99+
if resp == nil {
100+
t.Errorf("Response is nil after Select")
101+
}
102+
tupleValueIsDatetime(t, resp.Data, tm)
103+
104+
// Delete tuple with datetime.
105+
resp, err = conn.Delete(space, index, []interface{}{tm})
106+
if err != nil {
107+
t.Errorf("Datetime delete failed: %s", err.Error())
108+
}
109+
if resp == nil {
110+
t.Errorf("Response is nil after Delete")
111+
}
112+
tupleValueIsDatetime(t, resp.Data, tm)
113+
}
114+
115+
var datetimes = []string{
116+
"1970-01-01T00:00:00.000000010Z",
117+
"2010-08-12T11:39:14Z",
118+
"1984-03-24T18:04:05Z",
119+
"1970-01-01T00:00:00Z",
120+
"2010-01-12T00:00:00Z",
121+
"1970-01-01T00:00:00Z",
122+
"1970-01-01T00:00:00.123456789Z",
123+
"1970-01-01T00:00:00.12345678Z",
124+
"1970-01-01T00:00:00.1234567Z",
125+
"1970-01-01T00:00:00.123456Z",
126+
"1970-01-01T00:00:00.12345Z",
127+
"1970-01-01T00:00:00.1234Z",
128+
"1970-01-01T00:00:00.123Z",
129+
"1970-01-01T00:00:00.12Z",
130+
"1970-01-01T00:00:00.1Z",
131+
"1970-01-01T00:00:00.01Z",
132+
"1970-01-01T00:00:00.001Z",
133+
"1970-01-01T00:00:00.0001Z",
134+
"1970-01-01T00:00:00.00001Z",
135+
"1970-01-01T00:00:00.000001Z",
136+
"1970-01-01T00:00:00.0000001Z",
137+
"1970-01-01T00:00:00.00000001Z",
138+
"1970-01-01T00:00:00.000000001Z",
139+
"1970-01-01T00:00:00.000000009Z",
140+
"1970-01-01T00:00:00.00000009Z",
141+
"1970-01-01T00:00:00.0000009Z",
142+
"1970-01-01T00:00:00.000009Z",
143+
"1970-01-01T00:00:00.00009Z",
144+
"1970-01-01T00:00:00.0009Z",
145+
"1970-01-01T00:00:00.009Z",
146+
"1970-01-01T00:00:00.09Z",
147+
"1970-01-01T00:00:00.9Z",
148+
"1970-01-01T00:00:00.99Z",
149+
"1970-01-01T00:00:00.999Z",
150+
"1970-01-01T00:00:00.9999Z",
151+
"1970-01-01T00:00:00.99999Z",
152+
"1970-01-01T00:00:00.999999Z",
153+
"1970-01-01T00:00:00.9999999Z",
154+
"1970-01-01T00:00:00.99999999Z",
155+
"1970-01-01T00:00:00.999999999Z",
156+
"1970-01-01T00:00:00.0Z",
157+
"1970-01-01T00:00:00.00Z",
158+
"1970-01-01T00:00:00.000Z",
159+
"1970-01-01T00:00:00.0000Z",
160+
"1970-01-01T00:00:00.00000Z",
161+
"1970-01-01T00:00:00.000000Z",
162+
"1970-01-01T00:00:00.0000000Z",
163+
"1970-01-01T00:00:00.00000000Z",
164+
"1970-01-01T00:00:00.000000000Z",
165+
"1973-11-29T21:33:09Z",
166+
"2013-10-28T17:51:56Z",
167+
"9999-12-31T23:59:59Z",
168+
}
169+
170+
func TestDatetimeInsertSelectDelete(t *testing.T) {
171+
if isDatetimeSupported == false {
172+
t.Skip("Skipping test for Tarantool without datetime support in msgpack")
173+
}
174+
175+
conn := connectWithValidation(t)
176+
defer conn.Close()
177+
178+
for _, dt := range datetimes {
179+
tupleInsertSelectDelete(t, conn, dt)
180+
}
181+
}
182+
183+
func TestDatetimeReplace(t *testing.T) {
184+
if isDatetimeSupported == false {
185+
t.Skip("Skipping test for Tarantool without datetime support in msgpack")
186+
}
187+
188+
conn := connectWithValidation(t)
189+
defer conn.Close()
190+
191+
tm, err := time.Parse(time.RFC3339, "2007-01-02T15:04:05Z")
192+
if err != nil {
193+
t.Errorf("Time parse failed: %s", err)
194+
}
195+
196+
resp, err := conn.Replace(space, []interface{}{tm})
197+
if err != nil {
198+
t.Errorf("Datetime replace failed: %s", err)
199+
}
200+
if resp == nil {
201+
t.Errorf("Response is nil after Replace")
202+
}
203+
tupleValueIsDatetime(t, resp.Data, tm)
204+
205+
resp, err = conn.Select(space, index, 0, 1, IterEq, []interface{}{tm})
206+
if err != nil {
207+
t.Errorf("Datetime select failed: %s", err)
208+
}
209+
if resp == nil {
210+
t.Errorf("Response is nil after Select")
211+
}
212+
tupleValueIsDatetime(t, resp.Data, tm)
213+
214+
// Delete tuple with datetime.
215+
resp, err = conn.Delete(space, index, []interface{}{tm})
216+
if err != nil {
217+
t.Errorf("Datetime delete failed: %s", err.Error())
218+
}
219+
}
220+
221+
// runTestMain is a body of TestMain function
222+
// (see https://pkg.go.dev/testing#hdr-Main).
223+
// Using defer + os.Exit is not works so TestMain body
224+
// is a separate function, see
225+
// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls
226+
func runTestMain(m *testing.M) int {
227+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 10, 0)
228+
if err != nil {
229+
log.Fatalf("Failed to extract Tarantool version: %s", err)
230+
}
231+
232+
if isLess {
233+
log.Println("Skipping datetime tests...")
234+
isDatetimeSupported = false
235+
return m.Run()
236+
} else {
237+
isDatetimeSupported = true
238+
}
239+
240+
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
241+
InitScript: "config.lua",
242+
Listen: server,
243+
WorkDir: "work_dir",
244+
User: opts.User,
245+
Pass: opts.Pass,
246+
WaitStart: 100 * time.Millisecond,
247+
ConnectRetry: 3,
248+
RetryTimeout: 500 * time.Millisecond,
249+
})
250+
defer test_helpers.StopTarantoolWithCleanup(instance)
251+
252+
if err != nil {
253+
log.Fatalf("Failed to prepare test Tarantool: %s", err)
254+
}
255+
256+
return m.Run()
257+
}
258+
259+
func TestMain(m *testing.M) {
260+
code := runTestMain(m)
261+
os.Exit(code)
262+
}

0 commit comments

Comments
 (0)
Please sign in to comment.