Skip to content

Commit 5672651

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 d3b5696 commit 5672651

File tree

4 files changed

+479
-1
lines changed

4 files changed

+479
-1
lines changed

README.md

Lines changed: 46 additions & 1 deletion
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

Lines changed: 42 additions & 0 deletions
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

Lines changed: 129 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)