Skip to content

Datetime support #145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
tarantool:
- '1.10'
- '2.8'
- '2.9'
- '2.10'
- '2.x-latest'
coveralls: [false]
fuzzing: [false]
Expand All @@ -48,8 +48,8 @@ jobs:
- name: Clone the connector
uses: actions/checkout@v2

- name: Setup Tarantool ${{ matrix.tarantool }}
if: matrix.tarantool != '2.x-latest'
- name: Setup Tarantool ${{ matrix.tarantool }} (< 2.10)
if: matrix.tarantool != '2.x-latest' && matrix.tarantool != '2.10'
uses: tarantool/setup-tarantool@v1
with:
tarantool-version: ${{ matrix.tarantool }}
Expand All @@ -60,6 +60,12 @@ jobs:
curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash
sudo apt install -y tarantool tarantool-dev

- name: Setup Tarantool 2.10
if: matrix.tarantool == '2.10'
run: |
curl -L https://tarantool.io/tWsLBdI/release/2/installer.sh | bash
sudo apt install -y tarantool tarantool-dev

- name: Setup golang for the connector and tests
uses: actions/setup-go@v2
with:
Expand Down
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