diff --git a/README.md b/README.md index c27ee07ab..721958084 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,50 @@ func main() { } ``` +To enable support of UUID in msgpack with [google/uuid](https://github.com/google/uuid), +import tarantool/uuid submodule. +```go +package main + +import ( + "log" + "time" + + "github.com/tarantool/go-tarantool" + _ "github.com/tarantool/go-tarantool/uuid" + "github.com/google/uuid" +) + +func main() { + server := "127.0.0.1:3013" + opts := tarantool.Opts{ + Timeout: 500 * time.Millisecond, + Reconnect: 1 * time.Second, + MaxReconnects: 3, + User: "test", + Pass: "test", + } + client, err := tarantool.Connect(server, opts) + if err != nil { + log.Fatalf("Failed to connect: %s", err.Error()) + } + + spaceNo := uint32(524) + + id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") + if uuidErr != nil { + log.Fatalf("Failed to prepare uuid: %s", uuidErr) + } + + resp, err := client.Replace(spaceNo, []interface{}{ id }) + + log.Println("UUID tuple replace") + log.Println("Error", err) + log.Println("Code", resp.Code) + log.Println("Data", resp.Data) +} +``` + ## Schema ```go diff --git a/config.lua b/config.lua index 3c869cd76..96017160c 100644 --- a/config.lua +++ b/config.lua @@ -61,3 +61,24 @@ console.listen '0.0.0.0:33015' --box.schema.user.revoke('guest', 'read,write,execute', 'universe') +-- Create space with UUID pk if supported +local uuid = require('uuid') +local msgpack = require('msgpack') + +local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new()) +if uuid_msgpack_supported then + local suuid = box.schema.space.create('testUUID', { + id = 524, + if_not_exists = true, + }) + suuid:create_index('primary', { + type = 'tree', + parts = {{ field = 1, type = 'uuid' }}, + if_not_exists = true + }) + suuid:truncate() + + box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) + + suuid:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) +end diff --git a/go.mod b/go.mod index 80dcee6f9..604bc731c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.11 require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - google.golang.org/appengine v1.6.6 // indirect + google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/vmihailenco/msgpack.v2 v2.9.1 + gopkg.in/vmihailenco/msgpack.v2 v2.9.2 ) diff --git a/go.sum b/go.sum index 838f2f45a..7f493923c 100644 --- a/go.sum +++ b/go.sum @@ -12,9 +12,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/vmihailenco/msgpack.v2 v2.9.1 h1:kb0VV7NuIojvRfzwslQeP3yArBqJHW9tOl4t38VS1jM= -gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= diff --git a/uuid/config.lua b/uuid/config.lua new file mode 100644 index 000000000..34b5d26c4 --- /dev/null +++ b/uuid/config.lua @@ -0,0 +1,31 @@ +local uuid = require('uuid') +local msgpack = require('msgpack') + +box.cfg{ + listen = 3013, + wal_dir = 'xlog', + snap_dir = 'snap', +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) + +local uuid_msgpack_supported = pcall(msgpack.encode, uuid.new()) +if not uuid_msgpack_supported then + error('UUID unsupported, use Tarantool 2.4.1 or newer') +end + +local s = box.schema.space.create('testUUID', { + id = 524, + if_not_exists = true, +}) +s:create_index('primary', { + type = 'tree', + parts = {{ field = 1, type = 'uuid' }}, + if_not_exists = true +}) +s:truncate() + +box.schema.user.grant('test', 'read,write', 'space', 'testUUID', { if_not_exists = true }) + +s:insert({ uuid.fromstr("c8f0fa1f-da29-438c-a040-393f1126ad39") }) diff --git a/uuid/go.mod b/uuid/go.mod new file mode 100644 index 000000000..baee6648d --- /dev/null +++ b/uuid/go.mod @@ -0,0 +1,11 @@ +module github.com/tarantool/go-tarantool/uuid + +go 1.11 + +require ( + github.com/google/uuid v1.3.0 + github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6 + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/vmihailenco/msgpack.v2 v2.9.2 +) diff --git a/uuid/go.sum b/uuid/go.sum new file mode 100644 index 000000000..dc902998f --- /dev/null +++ b/uuid/go.sum @@ -0,0 +1,28 @@ +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6 h1:WGaVN8FgSHg3xaiYnJvTk9bnXIgW8nAOUg5aVH4RLX8= +github.com/tarantool/go-tarantool v0.0.0-20211104105631-61f3a41907b6/go.mod h1:m/mppmrDtgvS3tqUvaZRdRtlgzK1Gz/T6uGndkOItmQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= diff --git a/uuid/uuid.go b/uuid/uuid.go new file mode 100644 index 000000000..f50b6d92a --- /dev/null +++ b/uuid/uuid.go @@ -0,0 +1,57 @@ +package uuid + +import ( + "fmt" + "reflect" + + "github.com/google/uuid" + "gopkg.in/vmihailenco/msgpack.v2" +) + +// UUID external type +// Supported since Tarantool 2.4.1. See more in commit messages. +// https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5 + +const UUID_extId = 2 + +func encodeUUID(e *msgpack.Encoder, v reflect.Value) error { + id := v.Interface().(uuid.UUID) + + bytes, err := id.MarshalBinary() + if err != nil { + return fmt.Errorf("msgpack: can't marshal binary uuid: %w", err) + } + + _, err = e.Writer().Write(bytes) + if err != nil { + return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err) + } + + return nil +} + +func decodeUUID(d *msgpack.Decoder, v reflect.Value) error { + var bytesCount int = 16; + bytes := make([]byte, bytesCount) + + n, err := d.Buffered().Read(bytes) + if err != nil { + return fmt.Errorf("msgpack: can't read bytes on uuid decode: %w", err) + } + if n < bytesCount { + return fmt.Errorf("msgpack: unexpected end of stream after %d uuid bytes", n) + } + + id, err := uuid.FromBytes(bytes) + if err != nil { + return fmt.Errorf("msgpack: can't create uuid from bytes: %w", err) + } + + v.Set(reflect.ValueOf(id)) + return nil +} + +func init() { + msgpack.Register(reflect.TypeOf((*uuid.UUID)(nil)).Elem(), encodeUUID, decodeUUID) + msgpack.RegisterExt(UUID_extId, (*uuid.UUID)(nil)) +} diff --git a/uuid/uuid_test.go b/uuid/uuid_test.go new file mode 100644 index 000000000..61eb632d2 --- /dev/null +++ b/uuid/uuid_test.go @@ -0,0 +1,151 @@ +package uuid_test + +import ( + "fmt" + "testing" + "time" + + . "github.com/tarantool/go-tarantool" + _ "github.com/tarantool/go-tarantool/uuid" + "gopkg.in/vmihailenco/msgpack.v2" + "github.com/google/uuid" +) + +var server = "127.0.0.1:3013" +var opts = Opts{ + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", +} + +var space = "testUUID" +var index = "primary" + +type TupleUUID struct { + id uuid.UUID +} + +func (t *TupleUUID) 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) + } + res, err := d.DecodeInterface() + if err != nil { + return err + } + t.id = res.(uuid.UUID) + return nil +} + +func connectWithValidation(t *testing.T) *Connection { + conn, err := Connect(server, opts) + if err != nil { + t.Errorf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Errorf("conn is nil after Connect") + } + return conn +} + +func skipIfUUIDUnsupported(t *testing.T, conn *Connection) { + resp, err := conn.Eval("return pcall(require('msgpack').encode, require('uuid').new())", []interface{}{}) + if err != nil { + t.Errorf("Failed to Eval: %s", err.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Eval") + } + if len(resp.Data) < 1 { + t.Errorf("Response.Data is empty after Eval") + } + val := resp.Data[0].(bool) + if val != true { + t.Skip("Skipping test for Tarantool without UUID support in msgpack") + } +} + +func tupleValueIsId(t *testing.T, tuples []interface{}, id uuid.UUID) { + if len(tuples) != 1 { + t.Errorf("Response Data len != 1") + } + + if tpl, ok := tuples[0].([]interface{}); !ok { + t.Errorf("Unexpected return value body") + } else { + if len(tpl) != 1 { + t.Errorf("Unexpected return value body (tuple len)") + } + if val, ok := tpl[0].(uuid.UUID); !ok || val != id { + t.Errorf("Unexpected return value body (tuple 0 field)") + } + } +} + +func TestSelect(t *testing.T) { + conn := connectWithValidation(t) + defer conn.Close() + + skipIfUUIDUnsupported(t, conn) + + id, uuidErr := uuid.Parse("c8f0fa1f-da29-438c-a040-393f1126ad39") + if uuidErr != nil { + t.Errorf("Failed to prepare test uuid: %s", uuidErr) + } + + resp, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + if errSel != nil { + t.Errorf("UUID select failed: %s", errSel.Error()) + } + if resp == nil { + t.Errorf("Response is nil after Select") + } + tupleValueIsId(t, resp.Data, id) + + var tuples []TupleUUID + errTyp := conn.SelectTyped(space, index, 0, 1, IterEq, []interface{}{ id }, &tuples) + if errTyp != nil { + t.Errorf("Failed to SelectTyped: %s", errTyp.Error()) + } + if len(tuples) != 1 { + t.Errorf("Result len of SelectTyped != 1") + } + if tuples[0].id != id { + t.Errorf("Bad value loaded from SelectTyped: %s", tuples[0].id) + } +} + +func TestReplace(t *testing.T) { + conn := connectWithValidation(t) + defer conn.Close() + + skipIfUUIDUnsupported(t, conn) + + id, uuidErr := uuid.Parse("64d22e4d-ac92-4a23-899a-e59f34af5479") + if uuidErr != nil { + t.Errorf("Failed to prepare test uuid: %s", uuidErr) + } + + respRep, errRep := conn.Replace(space, []interface{}{ id }) + if errRep != nil { + t.Errorf("UUID replace failed: %s", errRep) + } + if respRep == nil { + t.Errorf("Response is nil after Replace") + } + tupleValueIsId(t, respRep.Data, id) + + respSel, errSel := conn.Select(space, index, 0, 1, IterEq, []interface{}{ id }) + if errSel != nil { + t.Errorf("UUID select failed: %s", errSel) + } + if respSel == nil { + t.Errorf("Response is nil after Select") + } + tupleValueIsId(t, respSel.Data, id) +}