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")