Skip to content

Commit c1750dc

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 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(mp_dt) -- d8047f80284f000000000a00000000000000 1. tarantool/tarantool#5946 2. tarantool/tarantool#6751 Closes #118
1 parent 7897baf commit c1750dc

File tree

6 files changed

+755
-0
lines changed

6 files changed

+755
-0
lines changed

CHANGELOG.md

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

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

datetime/config.lua

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
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:create_index('primary', {
35+
type = 'TREE',
36+
parts = {
37+
{ field = 1, type = 'datetime' },
38+
},
39+
if_not_exists = true
40+
})
41+
s_2:truncate()
42+
43+
box.schema.func.create('call_me_maybe')
44+
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_1', { if_not_exists = true })
45+
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_2', { if_not_exists = true })
46+
end)
47+
48+
local function call_me_maybe()
49+
return box.space.testDatetime_2:select()
50+
end
51+
rawset(_G, 'call_me_maybe', call_me_maybe)
52+
53+
-- Set listen only when every other thing is configured.
54+
box.cfg{
55+
listen = os.getenv("TEST_TNT_LISTEN"),
56+
}
57+
58+
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)