Skip to content

Commit 5c67d23

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]. Note that timezone's index and offset and intervals are not implemented in Tarantool, see [2] and [3]. 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(mp_dt) -- d8047f80284f000000000a00000000000000 1. tarantool/tarantool#5946 2. #163 3. #165 Closes #118
1 parent aca9121 commit 5c67d23

File tree

6 files changed

+842
-0
lines changed

6 files changed

+842
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1818
- queue-utube handling (#85)
1919
- Master discovery (#113)
2020
- SQL support (#62)
21+
- Support datetime type in msgpack (#118)
2122

2223
### Fixed
2324

Makefile

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ test-main:
5858
go clean -testcache
5959
go test . -v -p 1
6060

61+
.PHONY: test-datetime
62+
test-datetime:
63+
@echo "Running tests in datetime package"
64+
go clean -testcache
65+
go test ./datetime/ -v -p 1
66+
6167
.PHONY: coverage
6268
coverage:
6369
go clean -testcache

datetime/config.lua

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
local has_datetime, 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+
box.once("init", function()
17+
local s_1 = box.schema.space.create('testDatetime_1', {
18+
id = 524,
19+
if_not_exists = true,
20+
})
21+
s_1:create_index('primary', {
22+
type = 'TREE',
23+
parts = {
24+
{ field = 1, type = 'datetime' },
25+
},
26+
if_not_exists = true
27+
})
28+
s_1:truncate()
29+
30+
local s_2 = box.schema.space.create('testDatetime_2', {
31+
id = 525,
32+
if_not_exists = true,
33+
})
34+
s_2:format({
35+
{ 'Cid', type = 'unsigned' },
36+
{ 'Datetime', type = 'datetime' },
37+
{ 'Orig', type = 'unsigned' },
38+
{ 'Member', type = 'array' },
39+
})
40+
s_2:create_index('primary', {
41+
type = 'tree',
42+
parts = {
43+
{ field = 1, type = 'unsigned'},
44+
{ field = 2, type = 'datetime'},
45+
},
46+
if_not_exists = true
47+
})
48+
s_2:truncate()
49+
50+
local s_3 = box.schema.space.create('testDatetime_3', {
51+
id = 526,
52+
if_not_exists = true,
53+
})
54+
s_3:create_index('primary', {
55+
type = 'tree',
56+
parts = {
57+
{1, 'uint'}
58+
},
59+
if_not_exists = true
60+
})
61+
s_3:truncate()
62+
63+
box.schema.func.create('call_me_maybe')
64+
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_1', { if_not_exists = true })
65+
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_2', { if_not_exists = true })
66+
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_3', { if_not_exists = true })
67+
end)
68+
69+
local function call_me_maybe()
70+
local dt1 = datetime.new({ year = 1934 })
71+
local dt2 = datetime.new({ year = 1961 })
72+
local dt3 = datetime.new({ year = 1968 })
73+
return {
74+
{
75+
5, "Poyekhali!", {
76+
{dt1, "Klushino"},
77+
{dt2, "Baikonur"},
78+
{dt3, "Novoselovo"},
79+
},
80+
}
81+
}
82+
end
83+
rawset(_G, 'call_me_maybe', call_me_maybe)
84+
85+
-- Set listen only when every other thing is configured.
86+
box.cfg{
87+
listen = os.getenv("TEST_TNT_LISTEN"),
88+
}
89+
90+
require('console').start()

datetime/datetime.go

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

0 commit comments

Comments
 (0)