Skip to content

Commit 773fb90

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 2c3af56 commit 773fb90

File tree

6 files changed

+513
-1
lines changed

6 files changed

+513
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

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

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

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