Skip to content

Commit 2131871

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 2131871

File tree

6 files changed

+513
-1
lines changed

6 files changed

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

+46-1
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,49 @@ func main() {
331331
}
332332
```
333333

334+
To enable support of datetime in msgpack with builtin module [time](https://pkg.go.dev/time),
335+
import `tarantool/datetime` submodule.
336+
```go
337+
package main
338+
339+
import (
340+
"log"
341+
"time"
342+
343+
"github.com/tarantool/go-tarantool"
344+
_ "github.com/tarantool/go-tarantool/datetime"
345+
)
346+
347+
func main() {
348+
server := "127.0.0.1:3013"
349+
opts := tarantool.Opts{
350+
Timeout: 500 * time.Millisecond,
351+
Reconnect: 1 * time.Second,
352+
MaxReconnects: 3,
353+
User: "test",
354+
Pass: "test",
355+
}
356+
client, err := tarantool.Connect(server, opts)
357+
if err != nil {
358+
log.Fatalf("Failed to connect: %s", err.Error())
359+
}
360+
defer client.Close()
361+
362+
spaceNo := uint32(524)
363+
364+
tm, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
365+
if err != nil {
366+
log.Fatalf("Failed to parse time: %s", err)
367+
}
368+
369+
resp, err := client.Insert(spaceNo, []interface{}{tm})
370+
371+
log.Println("Error:", err)
372+
log.Println("Code:", resp.Code)
373+
log.Println("Data:", resp.Data)
374+
}
375+
```
376+
334377
## Schema
335378

336379
```go
@@ -700,9 +743,11 @@ and call
700743
```bash
701744
go clean -testcache && go test -v
702745
```
703-
Use the same for main `tarantool` package and `queue` and `uuid` subpackages.
746+
Use the same for main `tarantool` package, `queue`, `uuid` and `datetime` subpackages.
704747
`uuid` tests require
705748
[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5).
749+
`datetime` tests require
750+
[Tarantool 2.10 or newer](https://github.com/tarantool/tarantool/issues/5946).
706751

707752
## Alternative connectors
708753

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

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Package with datetime data type support.
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,
13+
// stored 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+
package datetime
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"math"
23+
"reflect"
24+
"time"
25+
26+
"encoding/binary"
27+
28+
"gopkg.in/vmihailenco/msgpack.v2"
29+
)
30+
31+
// Datetime external type
32+
// Supported since Tarantool 2.10. See more details in issue
33+
// https://github.com/tarantool/tarantool/issues/5946
34+
const datetime_extId = 4
35+
36+
/**
37+
* datetime structure keeps number of seconds and
38+
* nanoseconds since Unix Epoch.
39+
* Time is normalized by UTC, so time-zone offset
40+
* is informative only.
41+
*/
42+
type datetime struct {
43+
// Seconds since Epoch
44+
seconds int64
45+
// Nanoseconds, fractional part of seconds
46+
nsec int32
47+
// Timezone offset in minutes from UTC
48+
// (not implemented in Tarantool, https://github.com/tarantool/tarantool/issues/6751)
49+
tzOffset int16
50+
// Olson timezone id
51+
// (not implemented in Tarantool, https://github.com/tarantool/tarantool/issues/6751)
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 := tm.Nanosecond()
68+
dt.nsec = int32(math.Round((10000 * float64(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+
binary.LittleEndian.PutUint16(buf[tzOffsetSize:], uint16(dt.tzIndex))
83+
}
84+
85+
_, err := e.Writer().Write(buf)
86+
if err != nil {
87+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
88+
}
89+
90+
return nil
91+
}
92+
93+
func decodeDatetime(d *msgpack.Decoder, v reflect.Value) error {
94+
var dt datetime
95+
secondsBytes := make([]byte, secondsSize)
96+
n, err := d.Buffered().Read(secondsBytes)
97+
if err != nil {
98+
return fmt.Errorf("msgpack: can't read bytes on datetime's seconds decode: %w", err)
99+
}
100+
if n < secondsSize {
101+
return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n)
102+
}
103+
dt.seconds = int64(binary.LittleEndian.Uint64(secondsBytes))
104+
tailSize := nsecSize + tzOffsetSize + tzIndexSize
105+
tailBytes := make([]byte, tailSize)
106+
n, err = d.Buffered().Read(tailBytes)
107+
// Part with nanoseconds, tzoffset and tzindex is optional,
108+
// so we don't need to handle an error here.
109+
if err != nil && err != io.EOF {
110+
return fmt.Errorf("msgpack: can't read bytes on datetime's tail decode: %w", err)
111+
}
112+
dt.nsec = 0
113+
if err == nil {
114+
if n < tailSize {
115+
return fmt.Errorf("msgpack: can't read bytes on datetime's tail decode: %w", err)
116+
}
117+
dt.nsec = int32(binary.LittleEndian.Uint32(tailBytes[0:]))
118+
dt.tzOffset = int16(binary.LittleEndian.Uint16(tailBytes[nsecSize:]))
119+
dt.tzIndex = int16(binary.LittleEndian.Uint16(tailBytes[tzOffsetSize:]))
120+
}
121+
t := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
122+
v.Set(reflect.ValueOf(t))
123+
124+
return nil
125+
}
126+
127+
func init() {
128+
msgpack.Register(reflect.TypeOf((*time.Time)(nil)).Elem(), encodeDatetime, decodeDatetime)
129+
msgpack.RegisterExt(datetime_extId, (*time.Time)(nil))
130+
}

0 commit comments

Comments
 (0)