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 1cb237b

Browse files
committedApr 13, 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 7897baf commit 1cb237b

File tree

7 files changed

+487
-1
lines changed

7 files changed

+487
-1
lines changed
 

‎CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1616
- Support UUID type in msgpack (#90)
1717
- Go modules support (#91)
1818
- queue-utube handling (#85)
19+
- Support datetime type in msgpack (#118)
1920

2021
### Fixed
2122

‎Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ test:
1515
go clean -testcache
1616
go test ./... -v -p 1
1717

18+
.PHONY: test-datetime
19+
test-datetime:
20+
@echo "Running tests in datetime package"
21+
go clean -testcache
22+
go test ./datetime/ -v -p 1
23+
1824
.PHONY: coverage
1925
coverage:
2026
go clean -testcache

‎README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,9 +700,11 @@ and call
700700
```bash
701701
go clean -testcache && go test -v
702702
```
703-
Use the same for main `tarantool` package and `queue` and `uuid` subpackages.
703+
Use the same for main `tarantool` package, `queue`, `uuid` and `datetime` subpackages.
704704
`uuid` tests require
705705
[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5).
706+
`datetime` tests require
707+
[Tarantool 2.10 or newer](https://github.com/tarantool/tarantool/issues/5946).
706708

707709
## Alternative connectors
708710

‎datetime/config.lua

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

‎datetime/datetime.go

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

‎datetime/datetime_test.go

Lines changed: 262 additions & 0 deletions
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.Fatalf("Failed to connect: %s", err.Error())
57+
}
58+
if conn == nil {
59+
t.Fatalf("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.Fatalf("Unexpected return value body")
67+
} else {
68+
if len(tpl) != 1 {
69+
t.Fatalf("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.Fatalf("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.Fatalf("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.Fatalf("Datetime insert failed: %s", err.Error())
89+
}
90+
91+
// Select tuple with datetime.
92+
var offset uint32 = 0
93+
var limit uint32 = 1
94+
resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{tm})
95+
if err != nil {
96+
t.Fatalf("Datetime select failed: %s", err.Error())
97+
}
98+
if resp == nil {
99+
t.Fatalf("Response is nil after Select")
100+
}
101+
tupleValueIsDatetime(t, resp.Data, tm)
102+
103+
// Delete tuple with datetime.
104+
resp, err = conn.Delete(space, index, []interface{}{tm})
105+
if err != nil {
106+
t.Fatalf("Datetime delete failed: %s", err.Error())
107+
}
108+
if resp == nil {
109+
t.Fatalf("Response is nil after Delete")
110+
}
111+
tupleValueIsDatetime(t, resp.Data, tm)
112+
}
113+
114+
var datetimes = []string{
115+
"2012-01-31T23:59:59.000000010Z",
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.Fatalf("Time parse failed: %s", err)
194+
}
195+
196+
resp, err := conn.Replace(space, []interface{}{tm})
197+
if err != nil {
198+
t.Fatalf("Datetime replace failed: %s", err)
199+
}
200+
if resp == nil {
201+
t.Fatalf("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.Fatalf("Datetime select failed: %s", err)
208+
}
209+
if resp == nil {
210+
t.Fatalf("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.Fatalf("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+
}

‎datetime/example_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Run Tarantool instance before example execution.
2+
// Terminal 1:
3+
// $ cd datetime
4+
// $ TT_LISTEN=3013 TT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua
5+
//
6+
// Terminal 2:
7+
// $ cd datetime
8+
// $ go test -v example_test.go
9+
package datetime_test
10+
11+
import (
12+
"fmt"
13+
"time"
14+
15+
"github.com/tarantool/go-tarantool"
16+
_ "github.com/tarantool/go-tarantool/datetime"
17+
)
18+
19+
// Example demonstrates how to use tuples with datetime.
20+
func Example() {
21+
opts := tarantool.Opts{
22+
User: "test",
23+
Pass: "test",
24+
}
25+
conn, _ := tarantool.Connect("127.0.0.1:3013", opts)
26+
27+
var datetime = "2013-10-28T17:51:56Z"
28+
tm, _ := time.Parse(time.RFC3339, datetime)
29+
30+
// Insert a tuple with datetime.
31+
resp, err := conn.Insert(space, []interface{}{tm})
32+
fmt.Println("Error:", err)
33+
fmt.Println("Code:", resp.Code)
34+
fmt.Println("Data:", resp.Data)
35+
36+
// Select a tuple with datetime.
37+
var offset uint32 = 0
38+
var limit uint32 = 1
39+
conn.Select(space, index, offset, limit, tarantool.IterEq, []interface{}{tm})
40+
41+
// Delete a tuple with datetime.
42+
conn.Delete(space, index, []interface{}{tm})
43+
}

0 commit comments

Comments
 (0)
Please sign in to comment.