From 9d41e1e369ca6e565c01f0d9fe59fd383433aa0e Mon Sep 17 00:00:00 2001 From: vr009 Date: Thu, 3 Mar 2022 09:23:10 +0300 Subject: [PATCH] sql: add minimal sql support This patch adds the support of SQL in connector. Added support of positional and named arguments. Added ExecuteTyped() method for use with custom packing/unpacking for a type. Added all required constants to const.go for encoding SQL in msgpack and decoding response. Added SQL tests. Updated config.lua for creation the space for using SQL in tests. Added the check of Tarantool version to skip SQL tests if tarantool version < 2.0.0. Changed id of the test spaces with id=512 and id=514, cause if using SQL in tarantool there is no ability to set space id explicitly, so it gets created with id=512 by default and conflicts with already existing space with the same id. Added new dependency in go.sum, go.mod for using assert package. Added examples of using SQL queries in example_test.go for compiling the future documentation from sources. Added notes about the version since which Execute() is supported. Closes #62 --- CHANGELOG.md | 1 + config.lua | 20 +- connector.go | 1 + const.go | 16 + example_custom_unpacking_test.go | 2 +- example_test.go | 141 +++++++- multi/multi.go | 7 + request.go | 171 +++++++++ response.go | 108 +++++- tarantool_test.go | 575 ++++++++++++++++++++++++++++++- 10 files changed, 1012 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6168ede00..854ce6153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Go modules support (#91) - queue-utube handling (#85) - Master discovery (#113) +- SQL support (#62) ### Fixed diff --git a/config.lua b/config.lua index 06bec1303..d024b3a28 100644 --- a/config.lua +++ b/config.lua @@ -6,13 +6,25 @@ box.cfg{ box.once("init", function() local s = box.schema.space.create('test', { - id = 512, + id = 517, if_not_exists = true, }) s:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + local sp = box.schema.space.create('SQL_TEST', { + id = 519, + if_not_exists = true, + format = { + {name = "NAME0", type = "unsigned"}, + {name = "NAME1", type = "string"}, + {name = "NAME2", type = "string"}, + } + }) + sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true}) + sp:insert{1, "test", "test"} + local st = box.schema.space.create('schematest', { - id = 514, + id = 516, temporary = true, if_not_exists = true, field_count = 7, @@ -75,6 +87,10 @@ box.once("init", function() box.schema.user.grant('test', 'read,write', 'space', 'test') box.schema.user.grant('test', 'read,write', 'space', 'schematest') box.schema.user.grant('test', 'read,write', 'space', 'test_perf') + + -- grants for sql tests + box.schema.user.grant('test', 'create,read,write,drop,alter', 'space') + box.schema.user.grant('test', 'create', 'sequence') end) local function func_name() diff --git a/connector.go b/connector.go index d6d87eaca..0e79c6aaf 100644 --- a/connector.go +++ b/connector.go @@ -17,6 +17,7 @@ type Connector interface { Call(functionName string, args interface{}) (resp *Response, err error) Call17(functionName string, args interface{}) (resp *Response, err error) Eval(expr string, args interface{}) (resp *Response, err error) + Execute(expr string, args interface{}) (resp *Response, err error) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error) diff --git a/const.go b/const.go index 03b00c6b1..5152f8e43 100644 --- a/const.go +++ b/const.go @@ -11,6 +11,7 @@ const ( EvalRequest = 8 UpsertRequest = 9 Call17Request = 10 + ExecuteRequest = 11 PingRequest = 64 SubscribeRequest = 66 @@ -29,6 +30,19 @@ const ( KeyDefTuple = 0x28 KeyData = 0x30 KeyError = 0x31 + KeyMetaData = 0x32 + KeySQLText = 0x40 + KeySQLBind = 0x41 + KeySQLInfo = 0x42 + + KeyFieldName = 0x00 + KeyFieldType = 0x01 + KeyFieldColl = 0x02 + KeyFieldIsNullable = 0x03 + KeyIsAutoincrement = 0x04 + KeyFieldSpan = 0x05 + KeySQLInfoRowCount = 0x00 + KeySQLInfoAutoincrementIds = 0x01 // https://github.com/fl00r/go-tarantool-1.6/issues/2 @@ -49,4 +63,6 @@ const ( OkCode = uint32(0) ErrorCodeBit = 0x8000 PacketLengthBytes = 5 + ErSpaceExistsCode = 0xa + IteratorCode = 0x14 ) diff --git a/example_custom_unpacking_test.go b/example_custom_unpacking_test.go index a6f9ab55e..1bc955151 100644 --- a/example_custom_unpacking_test.go +++ b/example_custom_unpacking_test.go @@ -87,7 +87,7 @@ func Example_customUnpacking() { log.Fatalf("Failed to connect: %s", err.Error()) } - spaceNo := uint32(512) + spaceNo := uint32(517) indexNo := uint32(0) tuple := Tuple2{Cid: 777, Orig: "orig", Members: []Member{{"lol", "", 1}, {"wut", "", 3}}} diff --git a/example_test.go b/example_test.go index 0a6b6cb37..386ad11f2 100644 --- a/example_test.go +++ b/example_test.go @@ -2,6 +2,7 @@ package tarantool_test import ( "fmt" + "github.com/tarantool/go-tarantool/test_helpers" "time" "github.com/tarantool/go-tarantool" @@ -31,7 +32,8 @@ func ExampleConnection_Select() { conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) conn.Replace(spaceNo, []interface{}{uint(1112), "hallo", "werld"}) - resp, err := conn.Select(512, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) + resp, err := conn.Select(517, 0, 0, 100, tarantool.IterEq, []interface{}{uint(1111)}) + if err != nil { fmt.Printf("error in select is %v", err) return @@ -53,7 +55,9 @@ func ExampleConnection_SelectTyped() { conn := example_connect() defer conn.Close() var res []Tuple - err := conn.SelectTyped(512, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + + err := conn.SelectTyped(517, 0, 0, 100, tarantool.IterEq, tarantool.IntKey{1111}, &res) + if err != nil { fmt.Printf("error in select is %v", err) return @@ -73,6 +77,7 @@ func ExampleConnection_SelectTyped() { func ExampleConnection_SelectAsync() { conn := example_connect() defer conn.Close() + spaceNo := uint32(517) conn.Insert(spaceNo, []interface{}{uint(16), "test", "one"}) conn.Insert(spaceNo, []interface{}{uint(17), "test", "one"}) @@ -320,12 +325,12 @@ func ExampleSchema() { } space1 := schema.Spaces["test"] - space2 := schema.SpacesById[514] + space2 := schema.SpacesById[516] fmt.Printf("Space 1 ID %d %s\n", space1.Id, space1.Name) fmt.Printf("Space 2 ID %d %s\n", space2.Id, space2.Name) // Output: - // Space 1 ID 512 test - // Space 2 ID 514 schematest + // Space 1 ID 517 test + // Space 2 ID 516 schematest } // Example demonstrates how to retrieve information with space schema. @@ -344,7 +349,7 @@ func ExampleSpace() { // Access Space objects by name or ID. space1 := schema.Spaces["test"] - space2 := schema.SpacesById[514] // It's a map. + space2 := schema.SpacesById[516] // It's a map. fmt.Printf("Space 1 ID %d %s %s\n", space1.Id, space1.Name, space1.Engine) fmt.Printf("Space 1 ID %d %t\n", space1.FieldsCount, space1.Temporary) @@ -365,10 +370,132 @@ func ExampleSpace() { fmt.Printf("SpaceField 2 %s %s\n", spaceField2.Name, spaceField2.Type) // Output: - // Space 1 ID 512 test memtx + // Space 1 ID 517 test memtx // Space 1 ID 0 false // Index 0 primary // &{0 unsigned} &{2 string} // SpaceField 1 name0 unsigned // SpaceField 2 name3 unsigned } + +// To use SQL to query a tarantool instance, call Execute. +// +// Pay attention that with different types of queries (DDL, DQL, DML etc.) +// some fields of the response structure (MetaData and InfoAutoincrementIds in SQLInfo) may be nil. +func ExampleConnection_Execute() { + // Tarantool supports SQL since version 2.0.0 + isLess, _ := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if isLess { + return + } + 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 { + fmt.Printf("Failed to connect: %s", err.Error()) + } + + resp, err := client.Execute("CREATE TABLE SQL_TEST (id INTEGER PRIMARY KEY, name STRING)", []interface{}{}) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // there are 4 options to pass named parameters to an SQL query + // the simple map: + sqlBind1 := map[string]interface{}{ + "id": 1, + "name": "test", + } + + // any type of structure + sqlBind2 := struct { + Id int + Name string + }{1, "test"} + + // it is possible to use []tarantool.KeyValueBind + sqlBind3 := []interface{}{ + tarantool.KeyValueBind{Key: "id", Value: 1}, + tarantool.KeyValueBind{Key: "name", Value: "test"}, + } + + // or []interface{} slice with tarantool.KeyValueBind items inside + sqlBind4 := []tarantool.KeyValueBind{ + {"id", 1}, + {"name", "test"}, + } + + // the next usage + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind1) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the same as + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind2) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the same as + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind3) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the same as + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=:name", sqlBind4) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the way to pass positional arguments to an SQL query + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=? AND name=?", []interface{}{2, "test"}) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) + + // the way to pass SQL expression with using custom packing/unpacking for a type + var res []Tuple + sqlInfo, metaData, err := client.ExecuteTyped("SELECT id, name, name FROM SQL_TEST WHERE id=?", []interface{}{2}, &res) + fmt.Println("ExecuteTyped") + fmt.Println("Error", err) + fmt.Println("Data", res) + fmt.Println("MetaData", metaData) + fmt.Println("SQL Info", sqlInfo) + + // for using different types of parameters (positioned/named), collect all items in []interface{} + // all "named" items must be passed with tarantool.KeyValueBind{} + resp, err = client.Execute("SELECT id FROM SQL_TEST WHERE id=:id AND name=?", + []interface{}{tarantool.KeyValueBind{"id", 1}, "test"}) + fmt.Println("Execute") + fmt.Println("Error", err) + fmt.Println("Code", resp.Code) + fmt.Println("Data", resp.Data) + fmt.Println("MetaData", resp.MetaData) + fmt.Println("SQL Info", resp.SQLInfo) +} diff --git a/multi/multi.go b/multi/multi.go index c83010c36..89ec33ad6 100644 --- a/multi/multi.go +++ b/multi/multi.go @@ -340,6 +340,13 @@ func (connMulti *ConnectionMulti) Eval(expr string, args interface{}) (resp *tar return connMulti.getCurrentConnection().Eval(expr, args) } +// Execute passes sql expression to Tarantool for execution. +// +// Since 1.6.0 +func (connMulti *ConnectionMulti) Execute(expr string, args interface{}) (resp *tarantool.Response, err error) { + return connMulti.getCurrentConnection().Execute(expr, args) +} + // GetTyped performs select (with limit = 1 and offset = 0) to box space and // fills typed result. func (connMulti *ConnectionMulti) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) { diff --git a/request.go b/request.go index 6065959c4..dd9486ae1 100644 --- a/request.go +++ b/request.go @@ -2,6 +2,9 @@ package tarantool import ( "errors" + "reflect" + "strings" + "sync" "time" "gopkg.in/vmihailenco/msgpack.v2" @@ -120,6 +123,14 @@ func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err return conn.EvalAsync(expr, args).Get() } +// Execute passes sql expression to Tarantool for execution. +// +// It is equal to conn.ExecuteAsync(expr, args).Get(). +// Since 1.6.0 +func (conn *Connection) Execute(expr string, args interface{}) (resp *Response, err error) { + return conn.ExecuteAsync(expr, args).Get() +} + // single used for conn.GetTyped for decode one tuple. type single struct { res interface{} @@ -212,6 +223,16 @@ func (conn *Connection) EvalTyped(expr string, args interface{}, result interfac return conn.EvalAsync(expr, args).GetTyped(result) } +// ExecuteTyped passes sql expression to Tarantool for execution. +// +// In addition to error returns sql info and columns meta data +// Since 1.6.0 +func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) { + fut := conn.ExecuteAsync(expr, args) + err := fut.GetTyped(&result) + return fut.resp.SQLInfo, fut.resp.MetaData, err +} + // SelectAsync sends select request to Tarantool and returns Future. func (conn *Connection) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future { future := conn.newFuture(SelectRequest) @@ -346,10 +367,160 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future { }) } +// ExecuteAsync sends a sql expression for execution and returns Future. +// Since 1.6.0 +func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future { + future := conn.newFuture(ExecuteRequest) + return future.send(conn, func(enc *msgpack.Encoder) error { + enc.EncodeMapLen(2) + enc.EncodeUint64(KeySQLText) + enc.EncodeString(expr) + enc.EncodeUint64(KeySQLBind) + return encodeSQLBind(enc, args) + }) +} + +// KeyValueBind is a type for encoding named SQL parameters +type KeyValueBind struct { + Key string + Value interface{} +} + // // private // +// this map is needed for caching names of struct fields in lower case +// to avoid extra allocations in heap by calling strings.ToLower() +var lowerCaseNames sync.Map + +func encodeSQLBind(enc *msgpack.Encoder, from interface{}) error { + // internal function for encoding single map in msgpack + encodeKeyInterface := func(key string, val interface{}) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := enc.EncodeString(":" + key); err != nil { + return err + } + if err := enc.Encode(val); err != nil { + return err + } + return nil + } + + encodeKeyValue := func(key string, val reflect.Value) error { + if err := enc.EncodeMapLen(1); err != nil { + return err + } + if err := enc.EncodeString(":" + key); err != nil { + return err + } + if err := enc.EncodeValue(val); err != nil { + return err + } + return nil + } + + encodeNamedFromMap := func(mp map[string]interface{}) error { + if err := enc.EncodeSliceLen(len(mp)); err != nil { + return err + } + for k, v := range mp { + if err := encodeKeyInterface(k, v); err != nil { + return err + } + } + return nil + } + + encodeNamedFromStruct := func(val reflect.Value) error { + if err := enc.EncodeSliceLen(val.NumField()); err != nil { + return err + } + cached, ok := lowerCaseNames.Load(val.Type()) + if !ok { + fields := make([]string, val.NumField()) + for i := 0; i < val.NumField(); i++ { + key := val.Type().Field(i).Name + fields[i] = strings.ToLower(key) + v := val.Field(i) + if err := encodeKeyValue(fields[i], v); err != nil { + return err + } + } + lowerCaseNames.Store(val.Type(), fields) + return nil + } + + fields := cached.([]string) + for i := 0; i < val.NumField(); i++ { + k := fields[i] + v := val.Field(i) + if err := encodeKeyValue(k, v); err != nil { + return err + } + } + return nil + } + + encodeSlice := func(from interface{}) error { + castedSlice, ok := from.([]interface{}) + if !ok { + castedKVSlice := from.([]KeyValueBind) + t := len(castedKVSlice) + if err := enc.EncodeSliceLen(t); err != nil { + return err + } + for _, v := range castedKVSlice { + if err := encodeKeyInterface(v.Key, v.Value); err != nil { + return err + } + } + return nil + } + + if err := enc.EncodeSliceLen(len(castedSlice)); err != nil { + return err + } + for i := 0; i < len(castedSlice); i++ { + if kvb, ok := castedSlice[i].(KeyValueBind); ok { + k := kvb.Key + v := kvb.Value + if err := encodeKeyInterface(k, v); err != nil { + return err + } + } else { + if err := enc.Encode(castedSlice[i]); err != nil { + return err + } + } + } + return nil + } + + val := reflect.ValueOf(from) + switch val.Kind() { + case reflect.Map: + mp, ok := from.(map[string]interface{}) + if !ok { + return errors.New("failed to encode map: wrong format") + } + if err := encodeNamedFromMap(mp); err != nil { + return err + } + case reflect.Struct: + if err := encodeNamedFromStruct(val); err != nil { + return err + } + case reflect.Slice, reflect.Array: + if err := encodeSlice(from); err != nil { + return err + } + } + return nil +} + func (fut *Future) pack(h *smallWBuf, enc *msgpack.Encoder, body func(*msgpack.Encoder) error) (err error) { rid := fut.requestId hl := h.Len() diff --git a/response.go b/response.go index c56eaa483..9fcca64da 100644 --- a/response.go +++ b/response.go @@ -9,10 +9,94 @@ import ( type Response struct { RequestId uint32 Code uint32 - Error string // Error message. - // Data contains deserialized data for untyped requests. - Data []interface{} - buf smallBuf + Error string // error message + // Data contains deserialized data for untyped requests + Data []interface{} + MetaData []ColumnMetaData + SQLInfo SQLInfo + buf smallBuf +} + +type ColumnMetaData struct { + FieldName string + FieldType string + FieldCollation string + FieldIsNullable bool + FieldIsAutoincrement bool + FieldSpan string +} + +type SQLInfo struct { + AffectedCount uint64 + InfoAutoincrementIds []uint64 +} + +func (meta *ColumnMetaData) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeMapLen(); err != nil { + return err + } + if l == 0 { + return fmt.Errorf("map len doesn't match: %d", l) + } + for i := 0; i < l; i++ { + var mk uint64 + var mv interface{} + if mk, err = d.DecodeUint64(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + if mv, err = d.DecodeInterface(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + switch mk { + case KeyFieldName: + meta.FieldName = mv.(string) + case KeyFieldType: + meta.FieldType = mv.(string) + case KeyFieldColl: + meta.FieldCollation = mv.(string) + case KeyFieldIsNullable: + meta.FieldIsNullable = mv.(bool) + case KeyIsAutoincrement: + meta.FieldIsAutoincrement = mv.(bool) + case KeyFieldSpan: + meta.FieldSpan = mv.(string) + default: + return fmt.Errorf("failed to decode meta data") + } + } + return nil +} + +func (info *SQLInfo) DecodeMsgpack(d *msgpack.Decoder) error { + var err error + var l int + if l, err = d.DecodeMapLen(); err != nil { + return err + } + if l == 0 { + return fmt.Errorf("map len doesn't match") + } + for i := 0; i < l; i++ { + var mk uint64 + if mk, err = d.DecodeUint64(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + switch mk { + case KeySQLInfoRowCount: + if info.AffectedCount, err = d.DecodeUint64(); err != nil { + return fmt.Errorf("failed to decode meta data") + } + case KeySQLInfoAutoincrementIds: + if err = d.Decode(&info.InfoAutoincrementIds); err != nil { + return fmt.Errorf("failed to decode meta data") + } + default: + return fmt.Errorf("failed to decode meta data") + } + } + return nil } func (resp *Response) smallInt(d *msgpack.Decoder) (i int, err error) { @@ -86,6 +170,14 @@ func (resp *Response) decodeBody() (err error) { if resp.Error, err = d.DecodeString(); err != nil { return err } + case KeySQLInfo: + if err = d.Decode(&resp.SQLInfo); err != nil { + return err + } + case KeyMetaData: + if err = d.Decode(&resp.MetaData); err != nil { + return err + } default: if err = d.Skip(); err != nil { return err @@ -121,6 +213,14 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) { if resp.Error, err = d.DecodeString(); err != nil { return err } + case KeySQLInfo: + if err = d.Decode(&resp.SQLInfo); err != nil { + return err + } + case KeyMetaData: + if err = d.Decode(&resp.MetaData); err != nil { + return err + } default: if err = d.Skip(); err != nil { return err diff --git a/tarantool_test.go b/tarantool_test.go index fcfed56e4..3a6a839e9 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -2,8 +2,10 @@ package tarantool_test import ( "fmt" + "github.com/stretchr/testify/assert" "log" "os" + "reflect" "strings" "sync" "testing" @@ -52,7 +54,7 @@ func (m *Member) DecodeMsgpack(d *msgpack.Decoder) error { } var server = "127.0.0.1:3013" -var spaceNo = uint32(512) +var spaceNo = uint32(517) var spaceName = "test" var indexNo = uint32(0) var indexName = "primary" @@ -413,6 +415,74 @@ func BenchmarkClientLargeSelectParallel(b *testing.B) { }) } +func BenchmarkSQLParallel(b *testing.B) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + b.Fatal("Could not check the Tarantool version") + } + if isLess { + b.Skip() + } + + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("No connection available") + return + } + defer conn.Close() + + spaceNo := 519 + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("No connection available") + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } + } + }) +} + +func BenchmarkSQLSerial(b *testing.B) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + b.Fatal("Could not check the Tarantool version") + } + if isLess { + b.Skip() + } + + conn, err := Connect(server, opts) + if err != nil { + b.Errorf("Failed to connect: %s", err) + return + } + defer conn.Close() + + spaceNo := 519 + _, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"}) + if err != nil { + b.Errorf("Failed to replace: %s", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := conn.Execute("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?", []interface{}{uint(1111)}) + if err != nil { + b.Errorf("Select failed: %s", err.Error()) + break + } + } +} + /////////////////// func TestClient(t *testing.T) { @@ -728,6 +798,479 @@ func TestClient(t *testing.T) { } } +const ( + createTableQuery = "CREATE TABLE SQL_SPACE (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING COLLATE \"unicode\" DEFAULT NULL);" + insertQuery = "INSERT INTO SQL_SPACE VALUES (?, ?);" + selectNamedQuery = "SELECT id, name FROM SQL_SPACE WHERE id=:id AND name=:name;" + selectPosQuery = "SELECT id, name FROM SQL_SPACE WHERE id=? AND name=?;" + updateQuery = "UPDATE SQL_SPACE SET name=? WHERE id=?;" + enableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = true;" + selectSpanDifQuery = "SELECT id*2, name, id FROM SQL_SPACE WHERE name=?;" + alterTableQuery = "ALTER TABLE SQL_SPACE RENAME TO SQL_SPACE2;" + insertIncrQuery = "INSERT INTO SQL_SPACE2 VALUES (?, ?);" + deleteQuery = "DELETE FROM SQL_SPACE2 WHERE name=?;" + dropQuery = "DROP TABLE SQL_SPACE2;" + dropQuery2 = "DROP TABLE SQL_SPACE;" + disableFullMetaDataQuery = "SET SESSION \"sql_full_metadata\" = false;" + + selectTypedQuery = "SELECT NAME1, NAME0 FROM SQL_TEST WHERE NAME0=?" + selectNamedQuery2 = "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;" + selectPosQuery2 = "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=? AND NAME1=?;" + mixedQuery = "SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:name0 AND NAME1=?;" +) + +func TestSQL(t *testing.T) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + type testCase struct { + Query string + Args interface{} + Resp Response + } + + testCases := []testCase{ + { + createTableQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + insertQuery, + []interface{}{1, "test"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + selectNamedQuery, + map[string]interface{}{ + "id": 1, + "name": "test", + }, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{[]interface{}{uint64(1), "test"}}, + MetaData: []ColumnMetaData{ + {FieldType: "integer", FieldName: "ID"}, + {FieldType: "string", FieldName: "NAME"}}, + }, + }, + { + selectPosQuery, + []interface{}{1, "test"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{[]interface{}{uint64(1), "test"}}, + MetaData: []ColumnMetaData{ + {FieldType: "integer", FieldName: "ID"}, + {FieldType: "string", FieldName: "NAME"}}, + }, + }, + { + updateQuery, + []interface{}{"test_test", 1}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + enableFullMetaDataQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + selectSpanDifQuery, + []interface{}{"test_test"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, Data: []interface{}{[]interface{}{uint64(2), "test_test", uint64(1)}}, + MetaData: []ColumnMetaData{ + { + FieldType: "integer", + FieldName: "COLUMN_1", + FieldIsNullable: false, + FieldIsAutoincrement: false, + FieldSpan: "id*2", + }, + { + FieldType: "string", + FieldName: "NAME", + FieldIsNullable: true, + FieldIsAutoincrement: false, + FieldSpan: "name", + FieldCollation: "unicode", + }, + { + FieldType: "integer", + FieldName: "ID", + FieldIsNullable: false, + FieldIsAutoincrement: true, + FieldSpan: "id", + }, + }}, + }, + { + alterTableQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 0}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + insertIncrQuery, + []interface{}{2, "test_2"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1, InfoAutoincrementIds: []uint64{1}}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + deleteQuery, + []interface{}{"test_2"}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + dropQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + { + disableFullMetaDataQuery, + []interface{}{}, + Response{ + SQLInfo: SQLInfo{AffectedCount: 1}, + Data: []interface{}{}, + MetaData: nil, + }, + }, + } + + var conn *Connection + conn, err = Connect(server, opts) + assert.Nil(t, err, "Failed to Connect") + assert.NotNil(t, conn, "conn is nil after Connect") + defer conn.Close() + + for i, test := range testCases { + resp, err := conn.Execute(test.Query, test.Args) + assert.NoError(t, err, "Failed to Execute, Query number: %d", i) + assert.NotNil(t, resp, "Response is nil after Execute\nQuery number: %d", i) + for j := range resp.Data { + assert.Equal(t, resp.Data[j], test.Resp.Data[j], "Response data is wrong") + } + assert.Equal(t, resp.SQLInfo.AffectedCount, test.Resp.SQLInfo.AffectedCount, "Affected count is wrong") + + errorMsg := "Response Metadata is wrong" + for j := range resp.MetaData { + assert.Equal(t, resp.MetaData[j].FieldIsAutoincrement, test.Resp.MetaData[j].FieldIsAutoincrement, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldIsNullable, test.Resp.MetaData[j].FieldIsNullable, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldCollation, test.Resp.MetaData[j].FieldCollation, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldName, test.Resp.MetaData[j].FieldName, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldSpan, test.Resp.MetaData[j].FieldSpan, errorMsg) + assert.Equal(t, resp.MetaData[j].FieldType, test.Resp.MetaData[j].FieldType, errorMsg) + } + } +} + +func TestSQLTyped(t *testing.T) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatal("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatal("conn is nil after Connect") + } + defer conn.Close() + + mem := []Member{} + info, meta, err := conn.ExecuteTyped(selectTypedQuery, []interface{}{1}, &mem) + if info.AffectedCount != 0 { + t.Errorf("Rows affected count must be 0") + } + if len(meta) != 2 { + t.Errorf("Meta data is not full") + } + if len(mem) != 1 { + t.Errorf("Wrong length of result") + } + if err != nil { + t.Error(err) + } +} + +func TestSQLBindings(t *testing.T) { + // Data for test table + testData := map[int]string{ + 1: "test", + } + + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatal("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + var resp *Response + var conn *Connection + + conn, err = Connect(server, opts) + if err != nil { + t.Fatalf("Failed to connect: %s", err.Error()) + } + if conn == nil { + t.Fatal("conn is nil after Connect") + } + defer conn.Close() + + // test all types of supported bindings + // prepare named sql bind + sqlBind := map[string]interface{}{ + "id": 1, + "name": "test", + } + + sqlBind2 := struct { + Id int + Name string + }{1, "test"} + + sqlBind3 := []KeyValueBind{ + {"id", 1}, + {"name", "test"}, + } + + sqlBind4 := []interface{}{ + KeyValueBind{Key: "id", Value: 1}, + KeyValueBind{Key: "name", Value: "test"}, + } + + namedSQLBinds := []interface{}{ + sqlBind, + sqlBind2, + sqlBind3, + sqlBind4, + } + + //positioned sql bind + sqlBind5 := []interface{}{ + 1, "test", + } + + // mixed sql bind + sqlBind6 := []interface{}{ + KeyValueBind{Key: "name0", Value: 1}, + "test", + } + + for _, bind := range namedSQLBinds { + resp, err = conn.Execute(selectNamedQuery2, bind) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + t.Error("Select with named arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + } + + resp, err = conn.Execute(selectPosQuery2, sqlBind5) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + t.Error("Select with positioned arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } + + resp, err = conn.Execute(mixedQuery, sqlBind6) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if reflect.DeepEqual(resp.Data[0], []interface{}{1, testData[1]}) { + t.Error("Select with positioned arguments failed") + } + if resp.MetaData[0].FieldType != "unsigned" || + resp.MetaData[0].FieldName != "NAME0" || + resp.MetaData[1].FieldType != "string" || + resp.MetaData[1].FieldName != "NAME1" { + t.Error("Wrong metadata") + } +} + +func TestStressSQL(t *testing.T) { + // Tarantool supports SQL since version 2.0.0 + isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0) + if err != nil { + t.Fatalf("Could not check the Tarantool version") + } + if isLess { + t.Skip() + } + + var resp *Response + var conn *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") + } + defer conn.Close() + + resp, err = conn.Execute(createTableQuery, []interface{}{}) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code != 0 { + t.Fatalf("Failed to Execute: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // create table with the same name + resp, err = conn.Execute(createTableQuery, []interface{}{}) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code != ErSpaceExistsCode { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // execute with nil argument + resp, err = conn.Execute(createTableQuery, nil) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code == 0 { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // execute with zero string + resp, err = conn.Execute("", []interface{}{}) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code == 0 { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } + + // drop table query + resp, err = conn.Execute(dropQuery2, []interface{}{}) + if err != nil { + t.Fatalf("Failed to Execute: %s", err.Error()) + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code != 0 { + t.Fatalf("Failed to Execute: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 1 { + t.Errorf("Incorrect count of dropped spaces: %d", resp.SQLInfo.AffectedCount) + } + + // drop the same table + resp, err = conn.Execute(dropQuery2, []interface{}{}) + if err == nil { + t.Fatal("Unexpected lack of error") + } + if resp == nil { + t.Fatal("Response is nil after Execute") + } + if resp.Code == 0 { + t.Fatalf("Unexpected response code: %d", resp.Code) + } + if resp.SQLInfo.AffectedCount != 0 { + t.Errorf("Incorrect count of created spaces: %d", resp.SQLInfo.AffectedCount) + } +} + func TestSchema(t *testing.T) { var err error var conn *Connection @@ -751,29 +1294,29 @@ func TestSchema(t *testing.T) { } var space, space2 *Space var ok bool - if space, ok = schema.SpacesById[514]; !ok { - t.Errorf("space with id = 514 was not found in schema.SpacesById") + if space, ok = schema.SpacesById[516]; !ok { + t.Errorf("space with id = 516 was not found in schema.SpacesById") } if space2, ok = schema.Spaces["schematest"]; !ok { t.Errorf("space with name 'schematest' was not found in schema.SpacesById") } if space != space2 { - t.Errorf("space with id = 514 and space with name schematest are different") + t.Errorf("space with id = 516 and space with name schematest are different") } - if space.Id != 514 { - t.Errorf("space 514 has incorrect Id") + if space.Id != 516 { + t.Errorf("space 516 has incorrect Id") } if space.Name != "schematest" { - t.Errorf("space 514 has incorrect Name") + t.Errorf("space 516 has incorrect Name") } if !space.Temporary { - t.Errorf("space 514 should be temporary") + t.Errorf("space 516 should be temporary") } if space.Engine != "memtx" { - t.Errorf("space 514 engine should be memtx") + t.Errorf("space 516 engine should be memtx") } if space.FieldsCount != 7 { - t.Errorf("space 514 has incorrect fields count") + t.Errorf("space 516 has incorrect fields count") } if space.FieldsById == nil { @@ -883,20 +1426,20 @@ func TestSchema(t *testing.T) { } var rSpaceNo, rIndexNo uint32 - rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(514, 3) - if err != nil || rSpaceNo != 514 || rIndexNo != 3 { + rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex(516, 3) + if err != nil || rSpaceNo != 516 || rIndexNo != 3 { t.Errorf("numeric space and index params not resolved as-is") } - rSpaceNo, _, err = schema.ResolveSpaceIndex(514, nil) - if err != nil || rSpaceNo != 514 { + rSpaceNo, _, err = schema.ResolveSpaceIndex(516, nil) + if err != nil || rSpaceNo != 516 { t.Errorf("numeric space param not resolved as-is") } rSpaceNo, rIndexNo, err = schema.ResolveSpaceIndex("schematest", "secondary") - if err != nil || rSpaceNo != 514 || rIndexNo != 3 { + if err != nil || rSpaceNo != 516 || rIndexNo != 3 { t.Errorf("symbolic space and index params not resolved") } rSpaceNo, _, err = schema.ResolveSpaceIndex("schematest", nil) - if err != nil || rSpaceNo != 514 { + if err != nil || rSpaceNo != 516 { t.Errorf("symbolic space param not resolved") } _, _, err = schema.ResolveSpaceIndex("schematest22", "secondary")