Skip to content

Commit 6eb6112

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(dt, mp_dt) ``` 1. tarantool/tarantool#5946 2. tarantool/tarantool#6751 Closes #118
1 parent 7897baf commit 6eb6112

File tree

6 files changed

+482
-1
lines changed

6 files changed

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

README.md

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

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

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

0 commit comments

Comments
 (0)