From 563a6d6384d51856e054ed5a8035571ff165768b Mon Sep 17 00:00:00 2001 From: Sergey Bronnikov Date: Sat, 5 Mar 2022 10:48:09 +0300 Subject: [PATCH] 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 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. https://github.com/tarantool/tarantool/issues/5946 2. https://github.com/tarantool/go-tarantool/issues/163 3. https://github.com/tarantool/go-tarantool/issues/165 Closes #118 --- CHANGELOG.md | 1 + Makefile | 6 + datetime/config.lua | 69 +++++ datetime/datetime.go | 140 +++++++++ datetime/datetime_test.go | 578 ++++++++++++++++++++++++++++++++++++++ datetime/example_test.go | 77 +++++ decimal/decimal_test.go | 2 +- 7 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 datetime/config.lua create mode 100644 datetime/datetime.go create mode 100644 datetime/datetime_test.go create mode 100644 datetime/example_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4c72d53..bf3c3eb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 1c6e0ce18..7bc6411e4 100644 --- a/Makefile +++ b/Makefile @@ -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" diff --git a/datetime/config.lua b/datetime/config.lua new file mode 100644 index 000000000..8b1ba2316 --- /dev/null +++ b/datetime/config.lua @@ -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() diff --git a/datetime/datetime.go b/datetime/datetime.go new file mode 100644 index 000000000..e861da837 --- /dev/null +++ b/datetime/datetime.go @@ -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{}) +} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go new file mode 100644 index 000000000..e1aeef23d --- /dev/null +++ b/datetime/datetime_test.go @@ -0,0 +1,578 @@ +package datetime_test + +import ( + "encoding/hex" + "fmt" + "log" + "os" + "reflect" + "testing" + "time" + + . "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/test_helpers" + "gopkg.in/vmihailenco/msgpack.v2" +) + +var ( + minTime = time.Unix(0, 0) + maxTime = time.Unix(1<<63-1, 999999999) +) + +var isDatetimeSupported = false + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var spaceTuple1 = "testDatetime_1" +var spaceTuple2 = "testDatetime_2" +var index = "primary" + +func connectWithValidation(t *testing.T) *Connection { + t.Helper() + + conn, err := Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatalf("conn is nil after Connect") + } + return conn +} + +func skipIfDatetimeUnsupported(t *testing.T) { + t.Helper() + + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } +} + +// Expect that first element of tuple is time.Time. Compare extracted actual +// and expected datetime values. +func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { + t.Helper() + + dtIndex := 0 + if tpl, ok := tuples[dtIndex].([]interface{}); !ok { + t.Fatalf("Unexpected return value body") + } else { + if len(tpl) != 2 { + t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) + } + if val, ok := tpl[dtIndex].(Datetime); !ok || !val.ToTime().Equal(tm) { + t.Fatalf("Unexpected tuple %d field %v, expected %v", + dtIndex, + val.ToTime(), + tm) + } + } +} + +func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { + dt := NewDatetime(tm) + + // Insert tuple with datetime. + _, err := conn.Insert(spaceTuple1, []interface{}{dt, "payload"}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + // Select tuple with datetime. + var offset uint32 = 0 + var limit uint32 = 1 + resp, err := conn.Select(spaceTuple1, index, offset, limit, IterEq, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime select failed: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + assertDatetimeIsEqual(t, resp.Data, tm) + + // Delete tuple with datetime. + resp, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } + if resp == nil { + t.Fatalf("Response is nil after Delete") + } + assertDatetimeIsEqual(t, resp.Data, tm) +} + +var datetimeSample = []struct { + dt string + mpBuf string // MessagePack buffer. +}{ + {"2012-01-31T23:59:59.000000010Z", "d8047f80284f000000000a00000000000000"}, + {"1970-01-01T00:00:00.000000010Z", "d80400000000000000000a00000000000000"}, + {"2010-08-12T11:39:14Z", "d70462dd634c00000000"}, + {"1984-03-24T18:04:05Z", "d7041530c31a00000000"}, + {"2010-01-12T00:00:00Z", "d70480bb4b4b00000000"}, + {"1970-01-01T00:00:00Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.123456789Z", "d804000000000000000015cd5b0700000000"}, + {"1970-01-01T00:00:00.12345678Z", "d80400000000000000000ccd5b0700000000"}, + {"1970-01-01T00:00:00.1234567Z", "d8040000000000000000bccc5b0700000000"}, + {"1970-01-01T00:00:00.123456Z", "d804000000000000000000ca5b0700000000"}, + {"1970-01-01T00:00:00.12345Z", "d804000000000000000090b25b0700000000"}, + {"1970-01-01T00:00:00.1234Z", "d804000000000000000040ef5a0700000000"}, + {"1970-01-01T00:00:00.123Z", "d8040000000000000000c0d4540700000000"}, + {"1970-01-01T00:00:00.12Z", "d8040000000000000000000e270700000000"}, + {"1970-01-01T00:00:00.1Z", "d804000000000000000000e1f50500000000"}, + {"1970-01-01T00:00:00.01Z", "d80400000000000000008096980000000000"}, + {"1970-01-01T00:00:00.001Z", "d804000000000000000040420f0000000000"}, + {"1970-01-01T00:00:00.0001Z", "d8040000000000000000a086010000000000"}, + {"1970-01-01T00:00:00.00001Z", "d80400000000000000001027000000000000"}, + {"1970-01-01T00:00:00.000001Z", "d8040000000000000000e803000000000000"}, + {"1970-01-01T00:00:00.0000001Z", "d80400000000000000006400000000000000"}, + {"1970-01-01T00:00:00.00000001Z", "d80400000000000000000a00000000000000"}, + {"1970-01-01T00:00:00.000000001Z", "d80400000000000000000100000000000000"}, + {"1970-01-01T00:00:00.000000009Z", "d80400000000000000000900000000000000"}, + {"1970-01-01T00:00:00.00000009Z", "d80400000000000000005a00000000000000"}, + {"1970-01-01T00:00:00.0000009Z", "d80400000000000000008403000000000000"}, + {"1970-01-01T00:00:00.000009Z", "d80400000000000000002823000000000000"}, + {"1970-01-01T00:00:00.00009Z", "d8040000000000000000905f010000000000"}, + {"1970-01-01T00:00:00.0009Z", "d8040000000000000000a0bb0d0000000000"}, + {"1970-01-01T00:00:00.009Z", "d80400000000000000004054890000000000"}, + {"1970-01-01T00:00:00.09Z", "d8040000000000000000804a5d0500000000"}, + {"1970-01-01T00:00:00.9Z", "d804000000000000000000e9a43500000000"}, + {"1970-01-01T00:00:00.99Z", "d80400000000000000008033023b00000000"}, + {"1970-01-01T00:00:00.999Z", "d8040000000000000000c0878b3b00000000"}, + {"1970-01-01T00:00:00.9999Z", "d80400000000000000006043993b00000000"}, + {"1970-01-01T00:00:00.99999Z", "d8040000000000000000f0a29a3b00000000"}, + {"1970-01-01T00:00:00.999999Z", "d804000000000000000018c69a3b00000000"}, + {"1970-01-01T00:00:00.9999999Z", "d80400000000000000009cc99a3b00000000"}, + {"1970-01-01T00:00:00.99999999Z", "d8040000000000000000f6c99a3b00000000"}, + {"1970-01-01T00:00:00.999999999Z", "d8040000000000000000ffc99a3b00000000"}, + {"1970-01-01T00:00:00.0Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.00Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.0000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.00000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.000000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.0000000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.00000000Z", "d7040000000000000000"}, + {"1970-01-01T00:00:00.000000000Z", "d7040000000000000000"}, + {"1973-11-29T21:33:09Z", "d70415cd5b0700000000"}, + {"2013-10-28T17:51:56Z", "d7043ca46e5200000000"}, + {"9999-12-31T23:59:59Z", "d7047f41f4ff3a000000"}, +} + +func TestDatetimeInsertSelectDelete(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + for _, testcase := range datetimeSample { + t.Run(testcase.dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, testcase.dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) + } + tupleInsertSelectDelete(t, conn, tm) + }) + } +} + +// time.Parse() could not parse formatted string with datetime where year is +// bigger than 9999. That's why testcase with maximum datetime value represented +// as a separate testcase. Testcase with minimal value added for consistency. +func TestDatetimeMax(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, maxTime) +} + +func TestDatetimeMin(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, minTime) +} + +func TestDatetimeReplace(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tm, err := time.Parse(time.RFC3339, "2007-01-02T15:04:05Z") + if err != nil { + t.Fatalf("Time parse failed: %s", err) + } + + dt := NewDatetime(tm) + resp, err := conn.Replace(spaceTuple1, []interface{}{dt, "payload"}) + if err != nil { + t.Fatalf("Datetime replace failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Replace") + } + assertDatetimeIsEqual(t, resp.Data, tm) + + resp, err = conn.Select(spaceTuple1, index, 0, 1, IterEq, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime select failed: %s", err) + } + if resp == nil { + t.Fatalf("Response is nil after Select") + } + assertDatetimeIsEqual(t, resp.Data, tm) + + // Delete tuple with datetime. + _, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +type Event struct { + Datetime Datetime + Location string +} + +type Tuple2 struct { + Cid uint + Orig string + Events []Event +} + +type Tuple1 struct { + Datetime Datetime +} + +func (t *Tuple1) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.Encode(&t.Datetime); err != nil { + return err + } + return nil +} + +func (t *Tuple1) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 1 { + return fmt.Errorf("array len doesn't match: %d", l) + } + err = d.Decode(&t.Datetime) + if err != nil { + return err + } + return nil +} + +func (ev *Event) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.EncodeString(ev.Location); err != nil { + return err + } + if err := e.Encode(&ev.Datetime); err != nil { + return err + } + return nil +} + +func (ev *Event) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 2 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if ev.Location, err = d.DecodeString(); err != nil { + return err + } + res, err := d.DecodeInterface() + if err != nil { + return err + } + ev.Datetime = res.(Datetime) + return nil +} + +func (c *Tuple2) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(3); err != nil { + return err + } + if err := e.EncodeUint(c.Cid); err != nil { + return err + } + if err := e.EncodeString(c.Orig); err != nil { + return err + } + e.Encode(c.Events) + return nil +} + +func (c *Tuple2) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + if l != 3 { + return fmt.Errorf("array len doesn't match: %d", l) + } + if c.Cid, err = d.DecodeUint(); err != nil { + return err + } + if c.Orig, err = d.DecodeString(); err != nil { + return err + } + if l, err = d.DecodeSliceLen(); err != nil { + return err + } + c.Events = make([]Event, l) + for i := 0; i < l; i++ { + d.Decode(&c.Events[i]) + } + return nil +} + +func TestCustomEncodeDecodeTuple1(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + dt1, _ := time.Parse(time.RFC3339, "2010-05-24T17:51:56.000000009Z") + dt2, _ := time.Parse(time.RFC3339, "2022-05-24T17:51:56.000000009Z") + const cid = 13 + const orig = "orig" + + tuple := Tuple2{Cid: cid, + Orig: orig, + Events: []Event{ + {*NewDatetime(dt1), "Minsk"}, + {*NewDatetime(dt2), "Moscow"}, + }, + } + resp, err := conn.Replace(spaceTuple2, &tuple) + if err != nil || resp.Code != 0 { + t.Fatalf("Failed to replace: %s", err.Error()) + } + if len(resp.Data) != 1 { + t.Fatalf("Response Body len != 1") + } + + tpl, ok := resp.Data[0].([]interface{}) + if !ok { + t.Fatalf("Unexpected body of Replace") + } + + // Delete the tuple. + _, err = conn.Delete(spaceTuple2, index, []interface{}{cid}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } + + if len(tpl) != 3 { + t.Fatalf("Unexpected body of Replace (tuple len)") + } + if id, ok := tpl[0].(uint64); !ok || id != cid { + t.Fatalf("Unexpected body of Replace (%d)", cid) + } + if o, ok := tpl[1].(string); !ok || o != orig { + t.Fatalf("Unexpected body of Replace (%s)", orig) + } + + events, ok := tpl[2].([]interface{}) + if !ok { + t.Fatalf("Unable to convert 2 field to []interface{}") + } + + for i, tv := range []time.Time{dt1, dt2} { + dt := events[i].([]interface{})[1].(Datetime) + if !dt.ToTime().Equal(tv) { + t.Fatalf("%v != %v", dt.ToTime(), tv) + } + } +} + +func TestCustomDecodeFunction(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + // Call function 'call_datetime_testdata' returning a table of custom tuples. + var tuples []Tuple2 + err := conn.Call16Typed("call_datetime_testdata", []interface{}{1}, &tuples) + if err != nil { + t.Fatalf("Failed to CallTyped: %s", err.Error()) + } + + if cid := tuples[0].Cid; cid != 5 { + t.Fatalf("Wrong Cid (%d), should be 5", cid) + } + if orig := tuples[0].Orig; orig != "Go!" { + t.Fatalf("Wrong Orig (%s), should be 'Hello, there!'", orig) + } + + events := tuples[0].Events + if len(events) != 3 { + t.Fatalf("Wrong a number of Events (%d), should be 3", len(events)) + } + + locations := []string{ + "Klushino", + "Baikonur", + "Novoselovo", + } + + for i, ev := range events { + loc := ev.Location + dt := ev.Datetime + if loc != locations[i] || dt.ToTime().IsZero() { + t.Fatalf("Expected: %s non-zero time, got %s %v", + locations[i], + loc, + dt.ToTime()) + } + } +} + +func TestCustomEncodeDecodeTuple5(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := connectWithValidation(t) + defer conn.Close() + + tm := time.Unix(500, 1000) + dt := NewDatetime(tm) + _, err := conn.Insert(spaceTuple1, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + resp, errSel := conn.Select(spaceTuple1, index, 0, 1, IterEq, []interface{}{dt}) + if errSel != nil { + t.Errorf("Failed to Select: %s", errSel.Error()) + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Select") + } else { + if val, ok := tpl[0].(Datetime); !ok || !val.ToTime().Equal(tm) { + t.Fatalf("Unexpected body of Select") + } + } + + // Teardown: delete a value. + _, err = conn.Delete(spaceTuple1, index, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +func TestMPEncode(t *testing.T) { + for _, testcase := range datetimeSample { + t.Run(testcase.dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, testcase.dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) + } + dt := NewDatetime(tm) + buf, err := msgpack.Marshal(dt) + if err != nil { + t.Fatalf("Marshalling failed: %s", err.Error()) + } + refBuf, _ := hex.DecodeString(testcase.mpBuf) + if reflect.DeepEqual(buf, refBuf) != true { + t.Fatalf("Failed to encode datetime '%s', actual %v, expected %v", + testcase.dt, + buf, + refBuf) + } + }) + } +} + +func TestMPDecode(t *testing.T) { + for _, testcase := range datetimeSample { + t.Run(testcase.dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, testcase.dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", testcase.dt, err) + } + buf, _ := hex.DecodeString(testcase.mpBuf) + var v Datetime + err = msgpack.Unmarshal(buf, &v) + if err != nil { + t.Fatalf("Unmarshalling failed: %s", err.Error()) + } + if !tm.Equal(v.ToTime()) { + t.Fatalf("Failed to decode datetime buf '%s', actual %v, expected %v", + testcase.mpBuf, + testcase.dt, + v.ToTime()) + } + }) + } +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + log.Fatalf("Failed to extract Tarantool version: %s", err) + } + + if isLess { + log.Println("Skipping datetime tests...") + isDatetimeSupported = false + return m.Run() + } else { + isDatetimeSupported = true + } + + instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: server, + WorkDir: "work_dir", + User: opts.User, + Pass: opts.Pass, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(instance) + + if err != nil { + log.Fatalf("Failed to prepare test Tarantool: %s", err) + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/datetime/example_test.go b/datetime/example_test.go new file mode 100644 index 000000000..4dbba20dc --- /dev/null +++ b/datetime/example_test.go @@ -0,0 +1,77 @@ +// Run a Tarantool instance before example execution: +// Terminal 1: +// $ cd datetime +// $ TEST_TNT_LISTEN=3013 TEST_TNT_WORK_DIR=$(mktemp -d -t 'tarantool.XXX') tarantool config.lua +// +// Terminal 2: +// $ cd datetime +// $ go test -v example_test.go +package datetime_test + +import ( + "fmt" + "time" + + "github.com/tarantool/go-tarantool" + . "github.com/tarantool/go-tarantool/datetime" +) + +// Example demonstrates how to use tuples with datetime. To enable support of +// datetime import tarantool/datetime package. +func Example() { + opts := tarantool.Opts{ + User: "test", + Pass: "test", + } + conn, err := tarantool.Connect("127.0.0.1:3013", opts) + if err != nil { + fmt.Printf("error in connect is %v", err) + return + } + + var datetime = "2013-10-28T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("error in time.Parse() is %v", err) + return + } + dt := NewDatetime(tm) + + space := "testDatetime_1" + index := "primary" + + // Replace a tuple with datetime. + resp, err := conn.Replace(space, []interface{}{dt}) + if err != nil { + fmt.Printf("error in replace is %v", err) + return + } + respDt := resp.Data[0].([]interface{})[0].(Datetime) + fmt.Println("Datetime tuple replace") + fmt.Printf("Code: %d\n", resp.Code) + fmt.Printf("Data: %v\n", respDt.ToTime()) + + // Select a tuple with datetime. + var offset uint32 = 0 + var limit uint32 = 1 + resp, err = conn.Select(space, index, offset, limit, tarantool.IterEq, []interface{}{dt}) + if err != nil { + fmt.Printf("error in select is %v", err) + return + } + respDt = resp.Data[0].([]interface{})[0].(Datetime) + fmt.Println("Datetime tuple select") + fmt.Printf("Code: %d\n", resp.Code) + fmt.Printf("Data: %v\n", respDt.ToTime()) + + // Delete a tuple with datetime. + resp, err = conn.Delete(space, index, []interface{}{dt}) + if err != nil { + fmt.Printf("error in delete is %v", err) + return + } + respDt = resp.Data[0].([]interface{})[0].(Datetime) + fmt.Println("Datetime tuple delete") + fmt.Printf("Code: %d\n", resp.Code) + fmt.Printf("Data: %v\n", respDt.ToTime()) +} diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index d7d1091e6..eb5bcfe1c 100644 --- a/decimal/decimal_test.go +++ b/decimal/decimal_test.go @@ -29,7 +29,7 @@ func skipIfDecimalUnsupported(t *testing.T) { t.Helper() if isDecimalSupported == false { - t.Skip("Skipping test for Tarantool without datetime support in msgpack") + t.Skip("Skipping test for Tarantool without decimal support in msgpack") } }