diff --git a/CHANGELOG.md b/CHANGELOG.md index 854ce6153..b0a8ef706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - queue-utube handling (#85) - Master discovery (#113) - SQL support (#62) +- Support datetime type in msgpack (#118) ### Fixed diff --git a/Makefile b/Makefile index dfc38c496..ce1f1bb04 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/datetime/config.lua b/datetime/config.lua new file mode 100644 index 000000000..717373b12 --- /dev/null +++ b/datetime/config.lua @@ -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"), +} diff --git a/datetime/datetime.go b/datetime/datetime.go new file mode 100644 index 000000000..922aaad17 --- /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.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)) +} diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go new file mode 100644 index 000000000..6dc309476 --- /dev/null +++ b/datetime/datetime_test.go @@ -0,0 +1,543 @@ +package datetime_test + +import ( + "fmt" + "log" + "os" + "testing" + "time" + + . "github.com/tarantool/go-tarantool" + "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) +) + +// There is no way to skip tests in testing.M, +// so we use this variable to pass info +// to each testing.T that it should skip. +var isDatetimeSupported = false + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var space = "testDatetime_1" +var index = "primary" + +func connectWithValidation(t *testing.T) *Connection { + 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 +} + +// 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) { + dtIndex := 0 + if tpl, ok := tuples[dtIndex].([]interface{}); !ok { + t.Fatalf("Unexpected return value body") + } else { + if len(tpl) != 1 { + t.Fatalf("Unexpected return value body (tuple len = %d)", len(tpl)) + } + if val, ok := tpl[dtIndex].(time.Time); !ok || !val.Equal(tm) { + fmt.Println("Tuple: ", val) + fmt.Println("Expected:", tm) + t.Fatalf("Unexpected return value body (tuple %d field)", dtIndex) + } + } +} + +func tupleInsertSelectDelete(t *testing.T, conn *Connection, tm time.Time) { + // Insert tuple with datetime. + _, err := conn.Insert(space, []interface{}{tm}) + 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(space, index, offset, limit, IterEq, []interface{}{tm}) + 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(space, index, []interface{}{tm}) + 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 datetimes = []string{ + "2012-01-31T23:59:59.000000010Z", + "1970-01-01T00:00:00.000000010Z", + "2010-08-12T11:39:14Z", + "1984-03-24T18:04:05Z", + "2010-01-12T00:00:00Z", + "1970-01-01T00:00:00Z", + "1970-01-01T00:00:00.123456789Z", + "1970-01-01T00:00:00.12345678Z", + "1970-01-01T00:00:00.1234567Z", + "1970-01-01T00:00:00.123456Z", + "1970-01-01T00:00:00.12345Z", + "1970-01-01T00:00:00.1234Z", + "1970-01-01T00:00:00.123Z", + "1970-01-01T00:00:00.12Z", + "1970-01-01T00:00:00.1Z", + "1970-01-01T00:00:00.01Z", + "1970-01-01T00:00:00.001Z", + "1970-01-01T00:00:00.0001Z", + "1970-01-01T00:00:00.00001Z", + "1970-01-01T00:00:00.000001Z", + "1970-01-01T00:00:00.0000001Z", + "1970-01-01T00:00:00.00000001Z", + "1970-01-01T00:00:00.000000001Z", + "1970-01-01T00:00:00.000000009Z", + "1970-01-01T00:00:00.00000009Z", + "1970-01-01T00:00:00.0000009Z", + "1970-01-01T00:00:00.000009Z", + "1970-01-01T00:00:00.00009Z", + "1970-01-01T00:00:00.0009Z", + "1970-01-01T00:00:00.009Z", + "1970-01-01T00:00:00.09Z", + "1970-01-01T00:00:00.9Z", + "1970-01-01T00:00:00.99Z", + "1970-01-01T00:00:00.999Z", + "1970-01-01T00:00:00.9999Z", + "1970-01-01T00:00:00.99999Z", + "1970-01-01T00:00:00.999999Z", + "1970-01-01T00:00:00.9999999Z", + "1970-01-01T00:00:00.99999999Z", + "1970-01-01T00:00:00.999999999Z", + "1970-01-01T00:00:00.0Z", + "1970-01-01T00:00:00.00Z", + "1970-01-01T00:00:00.000Z", + "1970-01-01T00:00:00.0000Z", + "1970-01-01T00:00:00.00000Z", + "1970-01-01T00:00:00.000000Z", + "1970-01-01T00:00:00.0000000Z", + "1970-01-01T00:00:00.00000000Z", + "1970-01-01T00:00:00.000000000Z", + "1973-11-29T21:33:09Z", + "2013-10-28T17:51:56Z", + "9999-12-31T23:59:59Z", +} + +func TestDatetimeInsertSelectDelete(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + for _, dt := range datetimes { + t.Run(dt, func(t *testing.T) { + tm, err := time.Parse(time.RFC3339, dt) + if err != nil { + t.Fatalf("Time (%s) parse failed: %s", 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) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, MaxTime) +} + +func TestDatetimeMin(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + tupleInsertSelectDelete(t, conn, MinTime) +} + +func TestDatetimeReplace(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + 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) + } + + resp, err := conn.Replace(space, []interface{}{tm}) + 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(space, index, 0, 1, IterEq, []interface{}{tm}) + 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(space, index, []interface{}{tm}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +type Member struct { + Datetime time.Time + Location string +} + +type Tuple struct { + Cid uint + Orig string + Members []Member +} + +// Same effect in a "magic" way, but slower. +type Tuple2 struct { + _msgpack struct{} `msgpack:",asArray"` //nolint + + Cid uint + Orig string + Members []Member +} + +type Tuple3 struct { + Datetime time.Time +} + +func (t *Tuple3) 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 *Tuple3) 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 (m *Member) EncodeMsgpack(e *msgpack.Encoder) error { + if err := e.EncodeSliceLen(2); err != nil { + return err + } + if err := e.EncodeString(m.Location); err != nil { + return err + } + if err := e.Encode(m.Datetime); err != nil { + return err + } + return nil +} + +func (m *Member) 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 m.Location, err = d.DecodeString(); err != nil { + return err + } + res, err := d.DecodeInterface() + if err != nil { + return err + } + m.Datetime = res.(time.Time) + return nil +} + +func (c *Tuple) 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.Members) + return nil +} + +func (c *Tuple) 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.Members = make([]Member, l) + for i := 0; i < l; i++ { + d.Decode(&c.Members[i]) + } + return nil +} + +func TestCustomEncodeDecodeTuple1(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + tuple := Tuple{Cid: 13, Orig: "orig", Members: []Member{{time.Unix(12, 13), "Minsk"}, {time.Unix(13, 12), "Moscow"}}} + resp, err := conn.Replace("testDatetime_3", &tuple) + if err != nil || resp.Code != 0 { + t.Fatalf("Failed to replace: %s", err.Error()) + return + } + if len(resp.Data) != 1 { + t.Fatalf("Response Body len != 1") + } + if tpl, ok := resp.Data[0].([]interface{}); !ok { + t.Errorf("Unexpected body of Replace") + } else { + if len(tpl) != 3 { + t.Fatalf("Unexpected body of Replace (tuple len)") + } + if id, ok := tpl[0].(uint64); !ok || id != 13 { + t.Fatalf("Unexpected body of Replace (13)") + } + if o, ok := tpl[1].(string); !ok || o != "orig" { + t.Fatalf("Unexpected body of Replace (1)") + } + } +} + +func TestCustomEncodeDecodeTuple2(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + // Setup: insert a value. + tm := time.Unix(0, 0) + _, err := conn.Insert(space, []interface{}{tm}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + var tuples []Tuple + err = conn.SelectTyped("testDatetime_2", index, 0, 1, IterEq, []interface{}{1, tm}, &tuples) + if err != nil { + t.Fatalf("Failed to SelectTyped(): %s", err.Error()) + } + + // Teardown: delete a value. + _, err = conn.Delete(space, index, []interface{}{tm}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +func TestCustomEncodeDecodeTuple3(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + // Setup: insert a value. + tm := time.Unix(0, 0) + _, err := conn.Insert(space, []interface{}{tm}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + var tuples2 []Tuple2 + err = conn.SelectTyped("testDatetime_2", index, 0, 1, IterEq, []interface{}{1, tm}, &tuples2) + if err != nil { + log.Fatalf("Failed to SelectTyped: %s", err.Error()) + return + } + + // Teardown: delete a value. + _, err = conn.Delete(space, index, []interface{}{tm}) + if err != nil { + t.Fatalf("Datetime delete failed: %s", err.Error()) + } +} + +func TestCustomEncodeDecodeTuple4(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + // Setup: insert values. + _, err := conn.Insert(space, []interface{}{time.Unix(10, 0)}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + _, err = conn.Insert(space, []interface{}{time.Unix(11, 0)}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + _, err = conn.Insert(space, []interface{}{time.Unix(12, 0)}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + // Call function 'call_me_maybe' returning a table of custom tuples. + var tuples []Tuple + err = conn.CallTyped("call_me_maybe", []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 != "Hello, there!" { + t.Fatalf("Wrong Orig (%s), should be 'Hello, there!'", orig) + } +} + +func TestCustomEncodeDecodeTuple5(t *testing.T) { + if isDatetimeSupported == false { + t.Skip("Skipping test for Tarantool without datetime support in msgpack") + } + + conn := connectWithValidation(t) + defer conn.Close() + + dt := time.Unix(500, 1000) + _, err := conn.Insert(space, []interface{}{dt}) + if err != nil { + t.Fatalf("Datetime insert failed: %s", err.Error()) + } + + resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{dt}) + if errSel != nil { + t.Errorf("Failed to Select: %s", errSel.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + } +} + +// 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..71f51d041 --- /dev/null +++ b/datetime/example_test.go @@ -0,0 +1,56 @@ +// Run 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, _ := tarantool.Connect("127.0.0.1:3013", opts) + + var datetime = "2013-10-28T17:51:56.000000009Z" + tm, _ := time.Parse(time.RFC3339, datetime) + + space := "testDatetime" + index := "primary" + + // Replace a tuple with datetime. + resp, err := conn.Replace(space, []interface{}{tm}) + fmt.Println("Datetime tuple replace") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Select a tuple with datetime. + var offset uint32 = 0 + var limit uint32 = 1 + resp, err = conn.Select(space, index, offset, limit, tarantool.IterEq, []interface{}{tm}) + fmt.Println("Datetime tuple select") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + + // Delete a tuple with datetime. + resp, err = conn.Delete(space, index, []interface{}{tm}) + fmt.Println("Datetime tuple delete") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) +}