Skip to content

Commit 280d804

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 e2ffe23 commit 280d804

17 files changed

+626
-0
lines changed

CHANGELOG.md

+1
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

+21
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ type Connection struct {
143143

144144
var _ = Connector(&Connection{}) // Check compatibility with connector interface.
145145

146+
// ConnectedObject is an interface that provides the info about a Connection
147+
// the object belongs to.
148+
type ConnectedObject interface {
149+
Conn() *Connection
150+
}
151+
146152
type connShard struct {
147153
rmut sync.Mutex
148154
requests [requestsMap]struct {
@@ -993,6 +999,11 @@ func (conn *Connection) nextRequestId() (requestId uint32) {
993999
// An error is returned if the request was formed incorrectly, or failed to
9941000
// create the future.
9951001
func (conn *Connection) Do(req Request) *Future {
1002+
if stickyReq, ok := req.(ConnectedObject); ok {
1003+
if stickyReq.Conn() != conn {
1004+
return &Future{err: ErrStrangerConn}
1005+
}
1006+
}
9961007
return conn.send(req)
9971008
}
9981009

@@ -1009,3 +1020,13 @@ func (conn *Connection) OverrideSchema(s *Schema) {
10091020
conn.Schema = s
10101021
}
10111022
}
1023+
1024+
// NewPrepared passes a sql statement to Tarantool for preparation synchronously.
1025+
func (conn *Connection) NewPrepared(expr string) (*Prepared, error) {
1026+
req := NewPrepareRequest(expr)
1027+
resp, err := conn.Do(req).Get()
1028+
if err != nil {
1029+
return nil, err
1030+
}
1031+
return NewPreparedFromResponse(conn, resp), nil
1032+
}

connection_pool/config.lua

+15
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

+17
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,15 @@ func (connPool *ConnectionPool) EvalAsync(expr string, args interface{}, userMod
525525
}
526526

527527
// Do sends the request and returns a future.
528+
// For connected requests the argument of type Mode is unused.
528529
func (connPool *ConnectionPool) Do(req tarantool.Request, userMode Mode) *tarantool.Future {
530+
if stickyReq, ok := req.(tarantool.ConnectedObject); ok {
531+
conn, _ := connPool.getConnectionFromPool(stickyReq.Conn().Addr())
532+
if conn == nil {
533+
return newErrorFuture(tarantool.ErrStrangerConn)
534+
}
535+
return stickyReq.Conn().Do(req)
536+
}
529537
conn, err := connPool.getNextConnection(userMode)
530538
if err != nil {
531539
return newErrorFuture(err)
@@ -788,3 +796,12 @@ func newErrorFuture(err error) *tarantool.Future {
788796
fut.SetError(err)
789797
return fut
790798
}
799+
800+
// NewPrepared passes a sql statement to Tarantool for preparation synchronously.
801+
func (connPool *ConnectionPool) NewPrepared(expr string, userMode Mode) (*tarantool.Prepared, error) {
802+
conn, err := connPool.getNextConnection(userMode)
803+
if err != nil {
804+
return nil, err
805+
}
806+
return conn.NewPrepared(expr)
807+
}

connection_pool/connection_pool_test.go

+74
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,79 @@ func TestDo(t *testing.T) {
12761277
require.NotNilf(t, resp, "response is nil after Ping")
12771278
}
12781279

1280+
func TestNewPrepared(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.NewPrepared("SELECT NAME0, NAME1 FROM SQL_TEST WHERE NAME0=:id AND NAME1=:name;", connection_pool.RO)
1302+
require.Nilf(t, err, "fail to prepare statement: %v", err)
1303+
1304+
if connPool.GetPoolInfo()[stmt.Conn.Addr()].ConnRole != connection_pool.RO {
1305+
t.Errorf("wrong role for the statement's connection")
1306+
}
1307+
1308+
executeReq := tarantool.NewExecutePreparedRequest().WithPrepared(stmt)
1309+
unprepareReq := tarantool.NewUnprepareRequest().WithPrepared(stmt)
1310+
1311+
resp, err := connPool.Do(executeReq.Args([]interface{}{1, "test"}), connection_pool.ANY).Get()
1312+
if err != nil {
1313+
t.Fatalf("failed to execute prepared: %v", err)
1314+
}
1315+
if resp == nil {
1316+
t.Fatalf("nil response")
1317+
}
1318+
if resp.Code != tarantool.OkCode {
1319+
t.Fatalf("failed to execute prepared: code %d", resp.Code)
1320+
}
1321+
if reflect.DeepEqual(resp.Data[0], []interface{}{1, "test"}) {
1322+
t.Error("Select with named arguments failed")
1323+
}
1324+
if resp.MetaData[0].FieldType != "unsigned" ||
1325+
resp.MetaData[0].FieldName != "NAME0" ||
1326+
resp.MetaData[1].FieldType != "string" ||
1327+
resp.MetaData[1].FieldName != "NAME1" {
1328+
t.Error("Wrong metadata")
1329+
}
1330+
1331+
// the second argument for unprepare request is unused
1332+
resp, err = connPool.Do(unprepareReq, connection_pool.ANY).Get()
1333+
if err != nil {
1334+
t.Errorf("failed to unprepare prepared statement: %v", err)
1335+
}
1336+
if resp.Code != tarantool.OkCode {
1337+
t.Errorf("failed to unprepare prepared statement: code %d", resp.Code)
1338+
}
1339+
1340+
_, err = connPool.Do(unprepareReq, connection_pool.ANY).Get()
1341+
if err == nil {
1342+
t.Errorf("the statement must be already unprepared")
1343+
}
1344+
require.Contains(t, err.Error(), "Prepared statement with id")
1345+
1346+
_, err = connPool.Do(executeReq, connection_pool.ANY).Get()
1347+
if err == nil {
1348+
t.Errorf("the statement must be already unprepared")
1349+
}
1350+
require.Contains(t, err.Error(), "Prepared statement with id")
1351+
}
1352+
12791353
// runTestMain is a body of TestMain function
12801354
// (see https://pkg.go.dev/testing#hdr-Main).
12811355
// Using defer + os.Exit is not works so TestMain body

connection_pool/example_test.go

+25
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_NewPrepared() {
553+
pool, err := examplePool(testRoles)
554+
if err != nil {
555+
fmt.Println(err)
556+
}
557+
defer pool.Close()
558+
559+
stmt, err := pool.NewPrepared("SELECT 1", connection_pool.ANY)
560+
if err != nil {
561+
fmt.Println(err)
562+
}
563+
564+
executeReq := tarantool.NewExecutePreparedRequest().WithPrepared(stmt)
565+
unprepareReq := tarantool.NewUnprepareRequest().WithPrepared(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+
}

connector.go

+2
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,7 @@ type Connector interface {
4444
EvalAsync(expr string, args interface{}) *Future
4545
ExecuteAsync(expr string, args interface{}) *Future
4646

47+
NewPrepared(expr string) (*Prepared, error)
48+
4749
Do(req Request) (fut *Future)
4850
}

const.go

+3
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

+2
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

+40
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 NewPrepared.
656+
func ExampleConnection_NewPrepared() {
657+
// Tarantool supports SQL since version 2.0.0
658+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
659+
if err != nil || isLess {
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.NewPrepared("SELECT 1")
677+
if err != nil {
678+
fmt.Printf("Failed to connect: %s", err.Error())
679+
}
680+
681+
executeReq := tarantool.NewExecutePreparedRequest().WithPrepared(stmt)
682+
unprepareReq := tarantool.NewUnprepareRequest().WithPrepared(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

+18
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 Prepared, 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 Prepared) error {
94+
return fillUnprepare(enc, stmt)
95+
}

multi/multi.go

+14
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+
// NewPrepared passes a sql statement to Tarantool for preparation synchronously.
496+
func (connMulti *ConnectionMulti) NewPrepared(expr string) (*tarantool.Prepared, error) {
497+
return connMulti.getCurrentConnection().NewPrepared(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
}

prepared.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package tarantool
2+
3+
type PreparedID uint64
4+
5+
// Prepared is a type for handling prepared statements
6+
//
7+
// Since 1.7.0
8+
type Prepared struct {
9+
StatementID PreparedID
10+
MetaData []ColumnMetaData
11+
ParamCount uint64
12+
Conn *Connection
13+
}
14+
15+
func NewPreparedFromResponse(conn *Connection, resp *Response) *Prepared {
16+
stmt := resp.Data[0].(*Prepared)
17+
stmt.Conn = conn
18+
return stmt
19+
}

0 commit comments

Comments
 (0)