Skip to content

Commit f5b495d

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]. 1. tarantool/tarantool#5946 Closes #118
1 parent d3b5696 commit f5b495d

File tree

4 files changed

+432
-1
lines changed

4 files changed

+432
-1
lines changed

README.md

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

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

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 (not implemented)
50+
tzOffset int16
51+
// Olson timezone id (not implemented)
52+
tzIndex int16
53+
}
54+
55+
const (
56+
secondsSize = 8
57+
nsecSize = 4
58+
tzIndexSize = 2
59+
tzOffsetSize = 2
60+
)
61+
62+
func encodeDatetime(e *msgpack.Encoder, v reflect.Value) error {
63+
var dt datetime
64+
65+
tm := v.Interface().(time.Time)
66+
dt.seconds = tm.Unix()
67+
nsec := float64(tm.Nanosecond())
68+
dt.nsec = int32(math.Round((10000 * nsec) / 10000))
69+
dt.tzIndex = 0 /* not implemented */
70+
dt.tzOffset = 0 /* not implemented */
71+
72+
var bytesSize = secondsSize
73+
if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 {
74+
bytesSize += nsecSize + tzIndexSize + tzOffsetSize
75+
}
76+
77+
buf := make([]byte, bytesSize)
78+
binary.LittleEndian.PutUint64(buf[0:], uint64(dt.seconds))
79+
if bytesSize == 16 {
80+
binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec))
81+
binary.LittleEndian.PutUint16(buf[nsecSize:], uint16(dt.tzOffset))
82+
// FIXME
83+
//binary.LittleEndian.PutUint16(buf[tzOffsetSize:], uint16(dt.tzIndex))
84+
}
85+
86+
_, err := e.Writer().Write(buf)
87+
if err != nil {
88+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
89+
}
90+
91+
return nil
92+
}
93+
94+
func decodeDatetime(d *msgpack.Decoder, v reflect.Value) error {
95+
var dt datetime
96+
secondsBytes := make([]byte, secondsSize)
97+
n, err := d.Buffered().Read(secondsBytes)
98+
if err != nil {
99+
return fmt.Errorf("msgpack: can't read bytes on datetime's seconds decode: %w", err)
100+
}
101+
if n < secondsSize {
102+
return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n)
103+
}
104+
dt.seconds = int64(binary.LittleEndian.Uint64(secondsBytes))
105+
dt.nsec = 0
106+
tailSize := nsecSize + tzOffsetSize + tzIndexSize
107+
tailBytes := make([]byte, tailSize)
108+
n, err = d.Buffered().Read(tailBytes)
109+
// Part with nanoseconds, tzoffset and tzindex is optional,
110+
// so we don't need to handle an error here.
111+
if err == nil {
112+
if n < tailSize {
113+
return fmt.Errorf("msgpack: can't read bytes on datetime's tail decode: %w", err)
114+
}
115+
dt.nsec = int32(binary.LittleEndian.Uint32(tailBytes[0:]))
116+
dt.tzOffset = int16(binary.LittleEndian.Uint16(tailBytes[nsecSize:]))
117+
dt.tzIndex = int16(binary.LittleEndian.Uint16(tailBytes[tzOffsetSize:]))
118+
}
119+
t := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
120+
v.Set(reflect.ValueOf(t))
121+
122+
return nil
123+
}
124+
125+
func init() {
126+
msgpack.Register(reflect.TypeOf((*time.Time)(nil)).Elem(), encodeDatetime, decodeDatetime)
127+
msgpack.RegisterExt(Datetime_extId, (*time.Time)(nil))
128+
}

0 commit comments

Comments
 (0)