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 authored and oleg-jukovec committed Jun 23, 2022
1 parent 11c83c8 commit 563a6d6
Show file tree
Hide file tree
Showing 7 changed files with 872 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
- IPROTO_PUSH messages support (#67)
- Public API with request object types (#126)
- Support decimal type in msgpack (#96)
- Support datetime type in msgpack (#118)

### Changed

Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ test-connection-pool:
go clean -testcache
go test -tags "$(TAGS)" ./connection_pool/ -v -p 1

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

.PHONY: test-decimal
test-decimal:
@echo "Running tests in decimal package"
Expand Down
69 changes: 69 additions & 0 deletions datetime/config.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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_3 = box.schema.space.create('testDatetime_2', {
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_datetime_testdata')
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 })
end)

local function call_datetime_testdata()
local dt1 = datetime.new({ year = 1934 })
local dt2 = datetime.new({ year = 1961 })
local dt3 = datetime.new({ year = 1968 })
return {
{
5, "Go!", {
{"Klushino", dt1},
{"Baikonur", dt2},
{"Novoselovo", dt3},
},
}
}
end
rawset(_G, 'call_datetime_testdata', call_datetime_testdata)

-- Set listen only when every other thing is configured.
box.cfg{
listen = os.getenv("TEST_TNT_LISTEN"),
}

require('console').start()
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.7.0
//
// See also:
//
// * Datetime Internals https://github.com/tarantool/tarantool/wiki/Datetime-Internals
package datetime

import (
"encoding/binary"
"fmt"
"time"

"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
)

const maxSize = secondsSize + nsecSize + tzIndexSize + tzOffsetSize

type Datetime struct {
time time.Time
}

// NewDatetime returns a pointer to a new datetime.Datetime that contains a
// specified time.Time.
func NewDatetime(t time.Time) *Datetime {
dt := new(Datetime)
dt.time = t
return dt
}

// ToTime returns a time.Time that Datetime contains.
func (dtime *Datetime) ToTime() time.Time {
return dtime.time
}

var _ msgpack.Marshaler = (*Datetime)(nil)
var _ msgpack.Unmarshaler = (*Datetime)(nil)

func (dtime *Datetime) MarshalMsgpack() ([]byte, error) {
tm := dtime.ToTime()

var dt datetime
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, uint64(dt.seconds))
if bytesSize == maxSize {
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))
}

return buf, nil
}

func (tm *Datetime) UnmarshalMsgpack(b []byte) error {
l := len(b)
if l != maxSize && l != secondsSize {
return fmt.Errorf("invalid data length: got %d, wanted %d or %d", len(b), secondsSize, maxSize)
}

var dt datetime
sec := binary.LittleEndian.Uint64(b)
dt.seconds = int64(sec)
dt.nsec = 0
if l == maxSize {
dt.nsec = int32(binary.LittleEndian.Uint32(b[secondsSize:]))
dt.tzOffset = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize:]))
dt.tzIndex = int16(binary.LittleEndian.Uint16(b[secondsSize+nsecSize+tzOffsetSize:]))
}
tt := time.Unix(dt.seconds, int64(dt.nsec)).UTC()
*tm = *NewDatetime(tt)

return nil
}

func init() {
msgpack.RegisterExt(datetime_extId, &Datetime{})
}
Loading

0 comments on commit 563a6d6

Please sign in to comment.