Skip to content

Commit d96e7e1

Browse files
committed
sql: support prepared statements
This patch adds the support of prepared statements. Added a new type for handling prepared statements. Added new IPROTO-constants for support of prepared statements in const.go. Added benchmarks for SQL-select prepared statement. Added examples of using Prepare in example_test.go. Fixed some grammar inconsistencies for the method Execute. Updated CHANGELOG.md. Follows up #62 Closes #117
1 parent 62c3814 commit d96e7e1

15 files changed

+546
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1515
- Public API with request object types (#126)
1616
- Support decimal type in msgpack (#96)
1717
- Support datetime type in msgpack (#118)
18+
- Prepared SQL statements (#117)
1819

1920
### Changed
2021

connection.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,11 @@ func (conn *Connection) nextRequestId() (requestId uint32) {
993993
// An error is returned if the request was formed incorrectly, or failed to
994994
// create the future.
995995
func (conn *Connection) Do(req Request) *Future {
996+
if stickyReq, ok := req.(ConnectedObject); ok {
997+
if stickyReq.Conn() != conn {
998+
return &Future{err: ErrStrangerConn}
999+
}
1000+
}
9961001
return conn.send(req)
9971002
}
9981003

@@ -1009,3 +1014,19 @@ func (conn *Connection) OverrideSchema(s *Schema) {
10091014
conn.Schema = s
10101015
}
10111016
}
1017+
1018+
// NewPreparedStatement passes a sql statement to Tarantool for preparation synchronously.
1019+
func (conn *Connection) NewPreparedStatement(expr string) (*PreparedStatement, error) {
1020+
req := NewPrepareRequest(expr)
1021+
resp, err := conn.Do(req).Get()
1022+
if err != nil {
1023+
return nil, err
1024+
}
1025+
return NewPreparedStatementFromResponse(conn, resp), nil
1026+
}
1027+
1028+
// ConnectedObject is an interface that provides the info about a Connection
1029+
// the object belongs to.
1030+
type ConnectedObject interface {
1031+
Conn() *Connection
1032+
}

connection_pool/config.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ box.once("init", function()
2121
parts = {{ field = 1, type = 'string' }},
2222
if_not_exists = true
2323
})
24+
25+
local sp = box.schema.space.create('SQL_TEST', {
26+
id = 521,
27+
if_not_exists = true,
28+
format = {
29+
{name = "NAME0", type = "unsigned"},
30+
{name = "NAME1", type = "string"},
31+
{name = "NAME2", type = "string"},
32+
}
33+
})
34+
sp:create_index('primary', {type = 'tree', parts = {1, 'uint'}, if_not_exists = true})
35+
sp:insert{1, "test", "test"}
36+
-- grants for sql tests
37+
box.schema.user.grant('test', 'create,read,write,drop,alter', 'space')
38+
box.schema.user.grant('test', 'create', 'sequence')
2439
end)
2540

2641
local function simple_incr(a)

connection_pool/connection_pool.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,13 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod
526526

527527
// Do sends the request and returns a future.
528528
func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future {
529+
if stickyReq, ok := req.(tarantool.ConnectedObject); ok {
530+
conn, _ := connPool.getConnectionFromPool(stickyReq.Conn().Addr())
531+
if conn == nil {
532+
return newErrorFuture(tarantool.ErrStrangerConn)
533+
}
534+
return stickyReq.Conn().Do(req)
535+
}
529536
conn, err := connPool.getNextConnection(userMode)
530537
if err != nil {
531538
return newErrorFuture(err)
@@ -788,3 +795,12 @@ func newErrorFuture(err error) *tarantool.Future {
788795
fut.SetError(err)
789796
return fut
790797
}
798+
799+
// NewPreparedStatement passes a sql statement to Tarantool for preparation synchronously.
800+
func (connPool *ConnectionPool) NewPreparedStatement(expr string, userMode Mode) (*tarantool.PreparedStatement, error) {
801+
conn, err := connPool.getNextConnection(userMode)
802+
if err != nil {
803+
return nil, err
804+
}
805+
return conn.NewPreparedStatement(expr)
806+
}

connection_pool/connection_pool_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package connection_pool_test
33
import (
44
"log"
55
"os"
6+
"reflect"
67
"strings"
78
"testing"
89
"time"
@@ -1276,6 +1277,74 @@ func TestDo(t *testing.T) {
12761277
require.NotNilf(t, resp, "response is nil after Ping")
12771278
}
12781279

1280+
func TestNewPreparedStatement(t *testing.T) {
1281+
// Tarantool supports SQL since version 2.0.0
1282+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
1283+
if err != nil {
1284+
t.Fatalf("Could not check the Tarantool version")
1285+
}
1286+
if isLess {
1287+
t.Skip()
1288+
}
1289+
1290+
roles := []bool{true, true, false, true, false}
1291+
1292+
err = test_helpers.SetClusterRO(servers, connOpts, roles)
1293+
require.Nilf(t, err, "fail to set roles for cluster")
1294+
1295+
connPool, err := connection_pool.Connect(servers, connOpts)
1296+
require.Nilf(t, err, "failed to connect")
1297+
require.NotNilf(t, connPool, "conn is nil after Connect")
1298+
1299+
defer connPool.Close()
1300+
1301+
stmt, err := connPool.NewPreparedStatement("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", connection_pool.ANY)
1302+
require.Nilf(t, err, "fail to prepare statement: %v", err)
1303+
1304+
executeReq := tarantool.NewExecutePreparedRequest(stmt)
1305+
unprepareReq := tarantool.NewUnprepareRequest(stmt)
1306+
1307+
resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), connection_pool.ANY).Get()
1308+
if err != nil {
1309+
t.Fatalf("failed to execute prepared: %v", err)
1310+
}
1311+
if resp == nil {
1312+
t.Fatalf("nil response")
1313+
}
1314+
if resp.Code != tarantool.OkCode {
1315+
t.Fatalf("failed to execute prepared: code %d", resp.Code)
1316+
}
1317+
if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) {
1318+
t.Error("Select with named arguments failed")
1319+
}
1320+
if resp.MetaData[0].FieldType != "unsigned" ||
1321+
resp.MetaData[0].FieldName != "NAME0" ||
1322+
resp.MetaData[1].FieldType != "string" ||
1323+
resp.MetaData[1].FieldName != "NAME1" {
1324+
t.Error("Wrong metadata")
1325+
}
1326+
1327+
resp, err = connPool.Do(unprepareReq, connection_pool.ANY).Get()
1328+
if err != nil {
1329+
t.Errorf("failed to unprepare prepared statement: %v", err)
1330+
}
1331+
if resp.Code != tarantool.OkCode {
1332+
t.Errorf("failed to unprepare prepared statement: code %d", resp.Code)
1333+
}
1334+
1335+
_, err = connPool.Do(unprepareReq, connection_pool.ANY).Get()
1336+
if err == nil {
1337+
t.Errorf("the statement must be already unprepared")
1338+
}
1339+
require.Contains(t, err.Error(), "Prepared statement with id")
1340+
1341+
_, err = connPool.Do(executeReq, connection_pool.ANY).Get()
1342+
if err == nil {
1343+
t.Errorf("the statement must be already unprepared")
1344+
}
1345+
require.Contains(t, err.Error(), "Prepared statement with id")
1346+
}
1347+
12791348
// runTestMain is a body of TestMain function
12801349
// (see https://pkg.go.dev/testing#hdr-Main).
12811350
// Using defer + os.Exit is not works so TestMain body

connection_pool/example_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,28 @@ func ExampleConnectionPool_Do() {
548548
// Ping Data []
549549
// Ping Error <nil>
550550
}
551+
552+
func ExampleConnectionPool_NewPreparedStatement() {
553+
pool, err := examplePool(testRoles)
554+
if err != nil {
555+
fmt.Println(err)
556+
}
557+
defer pool.Close()
558+
559+
stmt, err := pool.NewPreparedStatement("SELECT 1", connection_pool.ANY)
560+
if err != nil {
561+
fmt.Println(err)
562+
}
563+
564+
executeReq := tarantool.NewExecutePreparedRequest(stmt)
565+
unprepareReq := tarantool.NewUnprepareRequest(stmt)
566+
567+
_, err = pool.Do(executeReq, connection_pool.ANY).Get()
568+
if err != nil {
569+
fmt.Printf("Failed to execute prepared stmt")
570+
}
571+
_, err = pool.Do(unprepareReq, connection_pool.ANY).Get()
572+
if err != nil {
573+
fmt.Printf("Failed to prepare")
574+
}
575+
}

const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
UpsertRequestCode = 9
1313
Call17RequestCode = 10 /* call in >= 1.7 format */
1414
ExecuteRequestCode = 11
15+
PrepareRequestCode = 13
1516
PingRequestCode = 64
1617
SubscribeRequestCode = 66
1718

@@ -31,9 +32,11 @@ const (
3132
KeyData = 0x30
3233
KeyError = 0x31
3334
KeyMetaData = 0x32
35+
KeyBindCount = 0x34
3436
KeySQLText = 0x40
3537
KeySQLBind = 0x41
3638
KeySQLInfo = 0x42
39+
KeyStmtID = 0x43
3740

3841
KeyFieldName = 0x00
3942
KeyFieldType = 0x01

errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,5 @@ const (
168168
ErrWrongSchemaVaersion = 109 // Wrong schema version, current: %d, in request: %u
169169
ErrSlabAllocMax = 110 // Failed to allocate %u bytes for tuple in the slab allocator: tuple is too large. Check 'slab_alloc_maximal' configuration option.
170170
)
171+
172+
var ErrStrangerConn = fmt.Errorf("the passed connected request doesn't belong to the current connection or connection pool")

example_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,43 @@ func ExampleConnection_Execute() {
651651
fmt.Println("MetaData", resp.MetaData)
652652
fmt.Println("SQL Info", resp.SQLInfo)
653653
}
654+
655+
// To use prepared statements to query a tarantool instance, call NewPreparedStatement.
656+
func ExampleConnection_NewPreparedStatement() {
657+
// Tarantool supports SQL since version 2.0.0
658+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
659+
if isLess || err != nil {
660+
return
661+
}
662+
663+
server := "127.0.0.1:3013"
664+
opts := tarantool.Opts{
665+
Timeout: 500 * time.Millisecond,
666+
Reconnect: 1 * time.Second,
667+
MaxReconnects: 3,
668+
User: "test",
669+
Pass: "test",
670+
}
671+
conn, err := tarantool.Connect(server, opts)
672+
if err != nil {
673+
fmt.Printf("Failed to connect: %s", err.Error())
674+
}
675+
676+
stmt, err := conn.NewPreparedStatement("SELECT 1")
677+
if err != nil {
678+
fmt.Printf("Failed to connect: %s", err.Error())
679+
}
680+
681+
executeReq := tarantool.NewExecutePreparedRequest(stmt)
682+
unprepareReq := tarantool.NewUnprepareRequest(stmt)
683+
684+
_, err = conn.Do(executeReq).Get()
685+
if err != nil {
686+
fmt.Printf("Failed to execute prepared stmt")
687+
}
688+
689+
_, err = conn.Do(unprepareReq).Get()
690+
if err != nil {
691+
fmt.Printf("Failed to prepare")
692+
}
693+
}

export_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,21 @@ func RefImplEvalBody(enc *msgpack.Encoder, expr string, args interface{}) error
7575
func RefImplExecuteBody(enc *msgpack.Encoder, expr string, args interface{}) error {
7676
return fillExecute(enc, expr, args)
7777
}
78+
79+
// RefImplPrepareBody is reference implementation for filling of an prepare
80+
// request's body.
81+
func RefImplPrepareBody(enc *msgpack.Encoder, expr string) error {
82+
return fillPrepare(enc, expr)
83+
}
84+
85+
// RefImplUnprepareBody is reference implementation for filling of an execute prepared
86+
// request's body.
87+
func RefImplExecutePreparedBody(enc *msgpack.Encoder, stmt PreparedStatement, args interface{}) error {
88+
return fillExecutePrepared(enc, stmt, args)
89+
}
90+
91+
// RefImplUnprepareBody is reference implementation for filling of an unprepare
92+
// request's body.
93+
func RefImplUnprepareBody(enc *msgpack.Encoder, stmt PreparedStatement) error {
94+
return fillUnprepare(enc, stmt)
95+
}

multi/multi.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,21 @@ func (connMulti *ConnectionMulti) ExecuteAsync(expr string, args interface{}) *t
492492
return connMulti.getCurrentConnection().ExecuteAsync(expr, args)
493493
}
494494

495+
// NewPreparedStatement passes a sql statement to Tarantool for preparation synchronously.
496+
func (connMulti *ConnectionMulti) NewPreparedStatement(expr string) (*tarantool.PreparedStatement, error) {
497+
return connMulti.getCurrentConnection().NewPreparedStatement(expr)
498+
}
499+
495500
// Do sends the request and returns a future.
496501
func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future {
502+
if stickyReq, ok := req.(tarantool.ConnectedObject); ok {
503+
_, belongs := connMulti.getConnectionFromPool(stickyReq.Conn().Addr())
504+
if !belongs {
505+
fut := &tarantool.Future{}
506+
fut.SetError(tarantool.ErrStrangerConn)
507+
return fut
508+
}
509+
return stickyReq.Conn().Do(req)
510+
}
497511
return connMulti.getCurrentConnection().Do(req)
498512
}

0 commit comments

Comments
 (0)