Skip to content

Commit

Permalink
datetime: add datetime type in msgpack
Browse files Browse the repository at this point in the history
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 and intervals are not implemented
in Tarantool, see [2] and [3].

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(mp_dt)  -- d8047f80284f000000000a00000000000000

1. tarantool/tarantool#5946
2. #163
3. #165

Closes #118
  • Loading branch information
ligurio committed May 18, 2022
1 parent 976020c commit 87b9c89
Show file tree
Hide file tree
Showing 6 changed files with 833 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
- Go modules support (#91)
- queue-utube handling (#85)
- Master discovery (#113)
- Support datetime type in msgpack (#118)

### Fixed

Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ test-main:
go clean -testcache
go test . -v -p 1

.PHONY: test-datetime
test-datetime:
@echo "Running tests in datetime package"
go clean -testcache
go test ./datetime/ -v -p 1

.PHONY: coverage
coverage:
go clean -testcache
Expand Down
87 changes: 87 additions & 0 deletions datetime/config.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
local has_datetime, datetime = pcall(require, 'datetime')

if not has_datetime then
error('Datetime unsupported, use Tarantool 2.10 or newer')
end

-- Do not set listen for now so connector won't be
-- able to send requests until everything is configured.
box.cfg{
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
}

box.schema.user.create('test', { password = 'test' , if_not_exists = true })
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })

box.once("init", function()
local s_1 = box.schema.space.create('testDatetime_1', {
id = 524,
if_not_exists = true,
})
s_1:create_index('primary', {
type = 'TREE',
parts = {
{ field = 1, type = 'datetime' },
},
if_not_exists = true
})
s_1:truncate()

local s_2 = box.schema.space.create('testDatetime_2', {
id = 525,
if_not_exists = true,
})
s_2:format({
{ 'Cid', type = 'unsigned' },
{ 'Datetime', type = 'datetime' },
{ 'Orig', type = 'unsigned' },
{ 'Member', type = 'array' },
})
s_2:create_index('primary', {
type = 'tree',
parts = {
{ field = 1, type = 'unsigned'},
{ field = 2, type = 'datetime'},
},
if_not_exists = true
})
s_2:truncate()

local s_3 = box.schema.space.create('testDatetime_3', {
id = 526,
if_not_exists = true,
})
s_3:create_index('primary', {
type = 'tree',
parts = {
{1, 'uint'}
},
if_not_exists = true
})
s_3:truncate()

box.schema.func.create('call_me_maybe')
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_1', { if_not_exists = true })
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_2', { if_not_exists = true })
box.schema.user.grant('test', 'read,write', 'space', 'testDatetime_3', { if_not_exists = true })
end)

local function call_me_maybe()
local dt1 = datetime.new({ year = 2022 })
local dt2 = datetime.new({ year = 1984 })
local dt3 = datetime.new({ year = 2019 })
return {
{5, "Hello, there!", {
{dt1, "Minsk"},
{dt2, "Moscow"},
{dt3, "Kiev"}
},
}
}
end
rawset(_G, 'call_me_maybe', call_me_maybe)

-- Set listen only when every other thing is configured.
box.cfg{
listen = os.getenv("TEST_TNT_LISTEN"),
}
140 changes: 140 additions & 0 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Package with support of Tarantool's datetime data type.
//
// Datetime data type supported in Tarantool since 2.10.
//
// Since: 1.6
//
// See also:
//
// * Datetime Internals https://github.com/tarantool/tarantool/wiki/Datetime-Internals
package datetime

import (
"fmt"
"io"
"reflect"
"time"

"encoding/binary"

"gopkg.in/vmihailenco/msgpack.v2"
)

// Datetime MessagePack serialization schema is an MP_EXT extension, which
// creates container of 8 or 16 bytes long payload.
//
// +---------+--------+===============+-------------------------------+
// |0xd7/0xd8|type (4)| seconds (8b) | nsec; tzoffset; tzindex; (8b) |
// +---------+--------+===============+-------------------------------+
//
// MessagePack data encoded using fixext8 (0xd7) or fixext16 (0xd8), and may
// contain:
//
// * [required] seconds parts as full, unencoded, signed 64-bit integer,
// stored in little-endian order;
//
// * [optional] all the other fields (nsec, tzoffset, tzindex) if any of them
// were having not 0 value. They are packed naturally in little-endian order;

// Datetime external type. Supported since Tarantool 2.10. See more details in
// issue https://github.com/tarantool/tarantool/issues/5946.
const datetime_extId = 4

// datetime structure keeps a number of seconds and nanoseconds since Unix Epoch.
// Time is normalized by UTC, so time-zone offset is informative only.
type datetime struct {
// Seconds since Epoch, where the epoch is the point where the time
// starts, and is platform dependent. For Unix, the epoch is January 1,
// 1970, 00:00:00 (UTC). Tarantool uses a double type, see a structure
// definition in src/lib/core/datetime.h and reasons in
// https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c
seconds int64
// Nanoseconds, fractional part of seconds. Tarantool uses int32_t, see
// a definition in src/lib/core/datetime.h.
nsec int32
// Timezone offset in minutes from UTC (not implemented in Tarantool,
// see gh-163). Tarantool uses a int16_t type, see a structure
// definition in src/lib/core/datetime.h.
tzOffset int16
// Olson timezone id (not implemented in Tarantool, see gh-163).
// Tarantool uses a int16_t type, see a structure definition in
// src/lib/core/datetime.h.
tzIndex int16
}

// Size of datetime fields in a MessagePack value.
const (
secondsSize = 8
nsecSize = 4
tzIndexSize = 2
tzOffsetSize = 2
)

func encodeDatetime(e *msgpack.Encoder, v reflect.Value) error {
var dt datetime

tm := v.Interface().(time.Time)
dt.seconds = tm.Unix()
dt.nsec = int32(tm.Nanosecond())
dt.tzIndex = 0 // It is not implemented, see gh-163.
dt.tzOffset = 0 // It is not implemented, see gh-163.

var bytesSize = secondsSize
if dt.nsec != 0 || dt.tzOffset != 0 || dt.tzIndex != 0 {
bytesSize += nsecSize + tzIndexSize + tzOffsetSize
}

buf := make([]byte, bytesSize)
binary.LittleEndian.PutUint64(buf[0:], uint64(dt.seconds))
if bytesSize == 16 {
binary.LittleEndian.PutUint32(buf[secondsSize:], uint32(dt.nsec))
binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize:], uint16(dt.tzOffset))
binary.LittleEndian.PutUint16(buf[secondsSize+nsecSize+tzOffsetSize:], uint16(dt.tzIndex))
}

_, err := e.Writer().Write(buf)
if err != nil {
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
}

return nil
}

func decodeDatetime(d *msgpack.Decoder, v reflect.Value) error {
var dt datetime
secondsBytes := make([]byte, secondsSize)
n, err := d.Buffered().Read(secondsBytes)
if err != nil {
return fmt.Errorf("msgpack: can't read bytes on datetime's seconds decode: %w", err)
}
if n < secondsSize {
return fmt.Errorf("msgpack: unexpected end of stream after %d datetime bytes", n)
}
dt.seconds = int64(binary.LittleEndian.Uint64(secondsBytes))
tailSize := nsecSize + tzOffsetSize + tzIndexSize
tailBytes := make([]byte, tailSize)
n, err = d.Buffered().Read(tailBytes)
// Part with nanoseconds, tzoffset and tzindex is optional, so we don't
// need to handle an error here.
if err != nil && err != io.EOF {
return fmt.Errorf("msgpack: can't read bytes on datetime's tail decode: %w", err)
}
dt.nsec = 0
if err == nil {
if n < tailSize {
return fmt.Errorf("msgpack: can't read bytes on datetime's tail decode: %w", err)
}
dt.nsec = int32(binary.LittleEndian.Uint32(tailBytes[0:]))
dt.tzOffset = int16(binary.LittleEndian.Uint16(tailBytes[nsecSize:]))
dt.tzIndex = int16(binary.LittleEndian.Uint16(tailBytes[nsecSize+tzOffsetSize:]))
}
t := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
v.Set(reflect.ValueOf(t))

return nil
}

func init() {
msgpack.Register(reflect.TypeOf((*time.Time)(nil)).Elem(), encodeDatetime, decodeDatetime)
msgpack.RegisterExt(datetime_extId, (*time.Time)(nil))
}
Loading

0 comments on commit 87b9c89

Please sign in to comment.