diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 09d6528a4..cf6ee8f3a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -31,7 +31,7 @@ jobs: tarantool: - '1.10' - '2.8' - - '2.9' + - '2.10' - '2.x-latest' coveralls: [false] fuzzing: [false] @@ -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 }} @@ -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: 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.go b/decimal/decimal.go index e6513af29..66587feec 100644 --- a/decimal/decimal.go +++ b/decimal/decimal.go @@ -37,29 +37,29 @@ const ( decimalPrecision = 38 ) -type DecNumber struct { +type Decimal struct { decimal.Decimal } -// NewDecNumber creates a new DecNumber from a decimal.Decimal. -func NewDecNumber(decimal decimal.Decimal) *DecNumber { - return &DecNumber{Decimal: decimal} +// NewDecimal creates a new Decimal from a decimal.Decimal. +func NewDecimal(decimal decimal.Decimal) *Decimal { + return &Decimal{Decimal: decimal} } -// NewDecNumberFromString creates a new DecNumber from a string. -func NewDecNumberFromString(src string) (result *DecNumber, err error) { +// NewDecimalFromString creates a new Decimal from a string. +func NewDecimalFromString(src string) (result *Decimal, err error) { dec, err := decimal.NewFromString(src) if err != nil { return } - result = NewDecNumber(dec) + result = NewDecimal(dec) return } -var _ msgpack.Marshaler = (*DecNumber)(nil) -var _ msgpack.Unmarshaler = (*DecNumber)(nil) +var _ msgpack.Marshaler = (*Decimal)(nil) +var _ msgpack.Unmarshaler = (*Decimal)(nil) -func (decNum *DecNumber) MarshalMsgpack() ([]byte, error) { +func (decNum *Decimal) MarshalMsgpack() ([]byte, error) { one := decimal.NewFromInt(1) maxSupportedDecimal := decimal.New(1, DecimalPrecision).Sub(one) // 10^DecimalPrecision - 1 minSupportedDecimal := maxSupportedDecimal.Neg().Sub(one) // -10^DecimalPrecision - 1 @@ -86,13 +86,13 @@ func (decNum *DecNumber) MarshalMsgpack() ([]byte, error) { // +--------+-------------------+------------+===============+ // | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | // +--------+-------------------+------------+===============+ -func (decNum *DecNumber) UnmarshalMsgpack(b []byte) error { +func (decNum *Decimal) UnmarshalMsgpack(b []byte) error { digits, err := decodeStringFromBCD(b) if err != nil { return fmt.Errorf("msgpack: can't decode string from BCD buffer (%x): %w", b, err) } dec, err := decimal.NewFromString(digits) - *decNum = *NewDecNumber(dec) + *decNum = *NewDecimal(dec) if err != nil { return fmt.Errorf("msgpack: can't encode string (%s) to a decimal number: %w", digits, err) } @@ -101,5 +101,5 @@ func (decNum *DecNumber) UnmarshalMsgpack(b []byte) error { } func init() { - msgpack.RegisterExt(decimalExtID, &DecNumber{}) + msgpack.RegisterExt(decimalExtID, &Decimal{}) } diff --git a/decimal/decimal_test.go b/decimal/decimal_test.go index 68bf299e0..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") } } @@ -37,7 +37,7 @@ var space = "testDecimal" var index = "primary" type TupleDecimal struct { - number DecNumber + number Decimal } func (t *TupleDecimal) EncodeMsgpack(e *msgpack.Encoder) error { @@ -61,7 +61,7 @@ func (t *TupleDecimal) DecodeMsgpack(d *msgpack.Decoder) error { if err != nil { return err } - t.number = res.(DecNumber) + t.number = res.(Decimal) return nil } @@ -138,7 +138,7 @@ var decimalSamples = []struct { func TestMPEncodeDecode(t *testing.T) { for _, testcase := range benchmarkSamples { t.Run(testcase.numString, func(t *testing.T) { - decNum, err := NewDecNumberFromString(testcase.numString) + decNum, err := NewDecimalFromString(testcase.numString) if err != nil { t.Fatal(err) } @@ -246,7 +246,7 @@ func TestEncodeStringToBCDIncorrectNumber(t *testing.T) { func TestEncodeMaxNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is bigger than maximum supported number (10^38 - 1)" decNum := decimal.New(1, DecimalPrecision) // // 10^DecimalPrecision - tuple := TupleDecimal{number: *NewDecNumber(decNum)} + tuple := TupleDecimal{number: *NewDecimal(decNum)} _, err := msgpack.Marshal(&tuple) if err == nil { t.Fatalf("It is possible to encode a number unsupported by Tarantool") @@ -260,7 +260,7 @@ func TestEncodeMinNumber(t *testing.T) { referenceErrMsg := "msgpack: decimal number is lesser than minimum supported number (-10^38 - 1)" two := decimal.NewFromInt(2) decNum := decimal.New(1, DecimalPrecision).Neg().Sub(two) // -10^DecimalPrecision - 2 - tuple := TupleDecimal{number: *NewDecNumber(decNum)} + tuple := TupleDecimal{number: *NewDecimal(decNum)} _, err := msgpack.Marshal(&tuple) if err == nil { t.Fatalf("It is possible to encode a number unsupported by Tarantool") @@ -279,7 +279,7 @@ func benchmarkMPEncodeDecode(b *testing.B, src decimal.Decimal, dst interface{}) var buf []byte var err error for i := 0; i < b.N; i++ { - tuple := TupleDecimal{number: *NewDecNumber(src)} + tuple := TupleDecimal{number: *NewDecimal(src)} if buf, err = msgpack.Marshal(&tuple); err != nil { b.Fatal(err) } @@ -304,7 +304,7 @@ func BenchmarkMPEncodeDecodeDecimal(b *testing.B) { func BenchmarkMPEncodeDecimal(b *testing.B) { for _, testcase := range benchmarkSamples { b.Run(testcase.numString, func(b *testing.B) { - decNum, err := NewDecNumberFromString(testcase.numString) + decNum, err := NewDecimalFromString(testcase.numString) if err != nil { b.Fatal(err) } @@ -319,7 +319,7 @@ func BenchmarkMPEncodeDecimal(b *testing.B) { func BenchmarkMPDecodeDecimal(b *testing.B) { for _, testcase := range benchmarkSamples { b.Run(testcase.numString, func(b *testing.B) { - decNum, err := NewDecNumberFromString(testcase.numString) + decNum, err := NewDecimalFromString(testcase.numString) if err != nil { b.Fatal(err) } @@ -361,7 +361,7 @@ func tupleValueIsDecimal(t *testing.T, tuples []interface{}, number decimal.Deci if len(tpl) != 1 { t.Fatalf("Unexpected return value body (tuple len)") } - if val, ok := tpl[0].(DecNumber); !ok || !val.Equal(number) { + if val, ok := tpl[0].(Decimal); !ok || !val.Equal(number) { t.Fatalf("Unexpected return value body (tuple 0 field)") } } @@ -426,9 +426,9 @@ func TestMPEncode(t *testing.T) { samples = append(samples, benchmarkSamples...) for _, testcase := range samples { t.Run(testcase.numString, func(t *testing.T) { - dec, err := NewDecNumberFromString(testcase.numString) + dec, err := NewDecimalFromString(testcase.numString) if err != nil { - t.Fatalf("NewDecNumberFromString() failed: %s", err.Error()) + t.Fatalf("NewDecimalFromString() failed: %s", err.Error()) } buf, err := msgpack.Marshal(dec) if err != nil { @@ -459,7 +459,7 @@ func TestMPDecode(t *testing.T) { if err != nil { t.Fatalf("Unmarshalling failed: %s", err.Error()) } - decActual := v.(DecNumber) + decActual := v.(Decimal) decExpected, err := decimal.NewFromString(testcase.numString) if err != nil { @@ -507,7 +507,7 @@ func TestSelect(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := conn.Insert(space, []interface{}{NewDecNumber(number)}) + resp, err := conn.Insert(space, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal insert failed: %s", err) } @@ -518,7 +518,7 @@ func TestSelect(t *testing.T) { var offset uint32 = 0 var limit uint32 = 1 - resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{NewDecNumber(number)}) + resp, err = conn.Select(space, index, offset, limit, IterEq, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal select failed: %s", err.Error()) } @@ -527,7 +527,7 @@ func TestSelect(t *testing.T) { } tupleValueIsDecimal(t, resp.Data, number) - resp, err = conn.Delete(space, index, []interface{}{NewDecNumber(number)}) + resp, err = conn.Delete(space, index, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal delete failed: %s", err) } @@ -540,7 +540,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { t.Fatalf("Failed to prepare test decimal: %s", err) } - resp, err := conn.Insert(space, []interface{}{NewDecNumber(number)}) + resp, err := conn.Insert(space, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal insert failed: %s", err) } @@ -549,7 +549,7 @@ func assertInsert(t *testing.T, conn *Connection, numString string) { } tupleValueIsDecimal(t, resp.Data, number) - resp, err = conn.Delete(space, index, []interface{}{NewDecNumber(number)}) + resp, err = conn.Delete(space, index, []interface{}{NewDecimal(number)}) if err != nil { t.Fatalf("Decimal delete failed: %s", err) } @@ -581,7 +581,7 @@ func TestReplace(t *testing.T) { t.Fatalf("Failed to prepare test decimal: %s", err) } - respRep, errRep := conn.Replace(space, []interface{}{NewDecNumber(number)}) + respRep, errRep := conn.Replace(space, []interface{}{NewDecimal(number)}) if errRep != nil { t.Fatalf("Decimal replace failed: %s", errRep) } @@ -590,7 +590,7 @@ func TestReplace(t *testing.T) { } tupleValueIsDecimal(t, respRep.Data, number) - respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{NewDecNumber(number)}) + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{NewDecimal(number)}) if errSel != nil { t.Fatalf("Decimal select failed: %s", errSel) } diff --git a/decimal/example_test.go b/decimal/example_test.go index f509b2492..1d335a4c3 100644 --- a/decimal/example_test.go +++ b/decimal/example_test.go @@ -35,7 +35,7 @@ func Example() { spaceNo := uint32(524) - number, err := NewDecNumberFromString("-22.804") + number, err := NewDecimalFromString("-22.804") if err != nil { log.Fatalf("Failed to prepare test decimal: %s", err) }