Skip to content

Commit 8f8f5f3

Browse files
committed
sql: support prepared statements
This patch adds the support of prepared statements. Changed the interface of the method Execute for accepting the first argument as a string (for sql queries) or uint64 (for statements id). Added new methods for connector's interface in connector.go. Added new IPROTO-constants for support of prepared statements in const.go. Updated multi-package for corresponding it to connector's interface. 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 2bc07ed commit 8f8f5f3

File tree

8 files changed

+239
-7
lines changed

8 files changed

+239
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1818
- queue-utube handling (#85)
1919
- Master discovery (#113)
2020
- SQL support (#62)
21+
- Prepared statements (#117)
2122

2223
### Fixed
2324

connector.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type Connector interface {
1717
Call(functionName string, args interface{}) (resp *Response, err error)
1818
Call17(functionName string, args interface{}) (resp *Response, err error)
1919
Eval(expr string, args interface{}) (resp *Response, err error)
20-
Execute(expr string, args interface{}) (resp *Response, err error)
20+
Execute(expr interface{}, args interface{}) (resp *Response, err error)
21+
Prepare(expr string) (resp *Response, err error)
2122

2223
GetTyped(space, index interface{}, key interface{}, result interface{}) (err error)
2324
SelectTyped(space, index interface{}, offset, limit, iterator uint32, key interface{}, result interface{}) (err error)
@@ -28,6 +29,7 @@ type Connector interface {
2829
CallTyped(functionName string, args interface{}, result interface{}) (err error)
2930
Call17Typed(functionName string, args interface{}, result interface{}) (err error)
3031
EvalTyped(expr string, args interface{}, result interface{}) (err error)
32+
ExecuteTyped(expr interface{}, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error)
3133

3234
SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future
3335
InsertAsync(space interface{}, tuple interface{}) *Future
@@ -38,4 +40,6 @@ type Connector interface {
3840
CallAsync(functionName string, args interface{}) *Future
3941
Call17Async(functionName string, args interface{}) *Future
4042
EvalAsync(expr string, args interface{}) *Future
43+
ExecuteAsync(expr interface{}, args interface{}) *Future
44+
PrepareAsync(expr string) *Future
4145
}

const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
UpsertRequest = 9
1313
Call17Request = 10
1414
ExecuteRequest = 11
15+
PrepareRequest = 13
1516
PingRequest = 64
1617
SubscribeRequest = 66
1718

@@ -34,6 +35,7 @@ const (
3435
KeySQLText = 0x40
3536
KeySQLBind = 0x41
3637
KeySQLInfo = 0x42
38+
KeyStmtID = 0x43
3739

3840
KeyFieldName = 0x00
3941
KeyFieldType = 0x01

example_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,42 @@ func ExampleConnection_Execute() {
499499
fmt.Println("MetaData", resp.MetaData)
500500
fmt.Println("SQL Info", resp.SQLInfo)
501501
}
502+
503+
// To use prepared statements to query a tarantool instance, call Prepare.
504+
func ExampleConnection_Prepare() {
505+
// Tarantool supports SQL since version 2.0.0
506+
isLess, _ := test_helpers.IsTarantoolVersionLess(2, 0, 0)
507+
if isLess {
508+
return
509+
}
510+
server := "127.0.0.1:3013"
511+
opts := tarantool.Opts{
512+
Timeout: 500 * time.Millisecond,
513+
Reconnect: 1 * time.Second,
514+
MaxReconnects: 3,
515+
User: "test",
516+
Pass: "test",
517+
}
518+
client, err := tarantool.Connect(server, opts)
519+
if err != nil {
520+
fmt.Printf("Failed to connect: %s", err.Error())
521+
}
522+
523+
// pass a query to prepare
524+
stmtResp, err := client.Prepare("SELECT id FROM SQL_TEST WHERE id=? AND name=?")
525+
fmt.Println("Prepare")
526+
fmt.Println("Error", err)
527+
fmt.Println("Code", stmtResp.Code)
528+
fmt.Println("Statement ID", stmtResp.StmtID)
529+
530+
stmtID := stmtResp.StmtID
531+
532+
// pass the id of the statement to Execute
533+
resp, err := client.Execute(stmtID, []interface{}{2, "test"})
534+
fmt.Println("Execute")
535+
fmt.Println("Error", err)
536+
fmt.Println("Code", resp.Code)
537+
fmt.Println("Data", resp.Data)
538+
fmt.Println("MetaData", resp.MetaData)
539+
fmt.Println("SQL Info", resp.SQLInfo)
540+
}

multi/multi.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,20 @@ func (connMulti *ConnectionMulti) Eval(expr string, args interface{}) (resp *tar
340340
return connMulti.getCurrentConnection().Eval(expr, args)
341341
}
342342

343-
// Execute passes sql expression to Tarantool for execution.
343+
// Execute passes a sql expression to Tarantool for execution.
344344
//
345345
// Since 1.6.0
346-
func (connMulti *ConnectionMulti) Execute(expr string, args interface{}) (resp *tarantool.Response, err error) {
346+
func (connMulti *ConnectionMulti) Execute(expr interface{}, args interface{}) (resp *tarantool.Response, err error) {
347347
return connMulti.getCurrentConnection().Execute(expr, args)
348348
}
349349

350+
// Prepare sends a sql statement to prepare.
351+
//
352+
// Since 1.6.0
353+
func (connMulti *ConnectionMulti) Prepare(expr string) (resp *tarantool.Response, err error) {
354+
return connMulti.getCurrentConnection().Prepare(expr)
355+
}
356+
350357
// GetTyped performs select (with limit = 1 and offset = 0) to box space and
351358
// fills typed result.
352359
func (connMulti *ConnectionMulti) GetTyped(space, index interface{}, key interface{}, result interface{}) (err error) {
@@ -401,6 +408,11 @@ func (connMulti *ConnectionMulti) EvalTyped(expr string, args interface{}, resul
401408
return connMulti.getCurrentConnection().EvalTyped(expr, args, result)
402409
}
403410

411+
// ExecuteTyped passes a sql expression for execution.
412+
func (connMulti *ConnectionMulti) ExecuteTyped(expr interface{}, args interface{}, result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) {
413+
return connMulti.getCurrentConnection().ExecuteTyped(expr, args, result)
414+
}
415+
404416
// SelectAsync sends select request to Tarantool and returns Future.
405417
func (connMulti *ConnectionMulti) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *tarantool.Future {
406418
return connMulti.getCurrentConnection().SelectAsync(space, index, offset, limit, iterator, key)
@@ -454,3 +466,17 @@ func (connMulti *ConnectionMulti) Call17Async(functionName string, args interfac
454466
func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tarantool.Future {
455467
return connMulti.getCurrentConnection().EvalAsync(expr, args)
456468
}
469+
470+
// ExecuteAsync passes a sql expression for execution.
471+
//
472+
// Since 1.6.0
473+
func (connMulti *ConnectionMulti) ExecuteAsync(expr interface{}, args interface{}) *tarantool.Future {
474+
return connMulti.getCurrentConnection().ExecuteAsync(expr, args)
475+
}
476+
477+
// PrepareAsync passes a SQL statement to prepare.
478+
//
479+
// Since 1.6.0
480+
func (connMulti *ConnectionMulti) PrepareAsync(expr string) *tarantool.Future {
481+
return connMulti.getCurrentConnection().PrepareAsync(expr)
482+
}

request.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,18 @@ func (conn *Connection) Eval(expr string, args interface{}) (resp *Response, err
127127
//
128128
// It is equal to conn.ExecuteAsync(expr, args).Get().
129129
// Since 1.6.0
130-
func (conn *Connection) Execute(expr string, args interface{}) (resp *Response, err error) {
130+
func (conn *Connection) Execute(expr interface{}, args interface{}) (resp *Response, err error) {
131131
return conn.ExecuteAsync(expr, args).Get()
132132
}
133133

134+
// Prepare sends a sql statement to prepare.
135+
//
136+
// It is equal to conn.PrepareAsync(expr, args).Get().
137+
// Since 1.6.0
138+
func (conn *Connection) Prepare(expr string) (resp *Response, err error) {
139+
return conn.PrepareAsync(expr).Get()
140+
}
141+
134142
// single used for conn.GetTyped for decode one tuple.
135143
type single struct {
136144
res interface{}
@@ -227,7 +235,7 @@ func (conn *Connection) EvalTyped(expr string, args interface{}, result interfac
227235
//
228236
// In addition to error returns sql info and columns meta data
229237
// Since 1.6.0
230-
func (conn *Connection) ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) {
238+
func (conn *Connection) ExecuteTyped(expr interface{}, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error) {
231239
fut := conn.ExecuteAsync(expr, args)
232240
err := fut.GetTyped(&result)
233241
return fut.resp.SQLInfo, fut.resp.MetaData, err
@@ -369,17 +377,45 @@ func (conn *Connection) EvalAsync(expr string, args interface{}) *Future {
369377

370378
// ExecuteAsync sends a sql expression for execution and returns Future.
371379
// Since 1.6.0
372-
func (conn *Connection) ExecuteAsync(expr string, args interface{}) *Future {
380+
func (conn *Connection) ExecuteAsync(expr interface{}, args interface{}) *Future {
373381
future := conn.newFuture(ExecuteRequest)
382+
var stmtID uint64
383+
exprStr, ok := expr.(string)
384+
if !ok {
385+
stmtID, ok = expr.(uint64)
386+
if !ok {
387+
err := errors.New("expr argument must be either string or uint64")
388+
return NewErrorFuture(err)
389+
}
390+
return future.send(conn, func(enc *msgpack.Encoder) error {
391+
enc.EncodeMapLen(2)
392+
enc.EncodeUint64(KeyStmtID)
393+
enc.EncodeUint64(stmtID)
394+
enc.EncodeUint64(KeySQLBind)
395+
return encodeSQLBind(enc, args)
396+
})
397+
}
374398
return future.send(conn, func(enc *msgpack.Encoder) error {
375399
enc.EncodeMapLen(2)
376400
enc.EncodeUint64(KeySQLText)
377-
enc.EncodeString(expr)
401+
enc.EncodeString(exprStr)
378402
enc.EncodeUint64(KeySQLBind)
379403
return encodeSQLBind(enc, args)
380404
})
381405
}
382406

407+
// PrepareAsync sends a sql statement to prepare and returns Future.
408+
//
409+
// Since 1.6.0
410+
func (conn *Connection) PrepareAsync(expr string) *Future {
411+
future := conn.newFuture(PrepareRequest)
412+
return future.send(conn, func(enc *msgpack.Encoder) error {
413+
enc.EncodeMapLen(1)
414+
enc.EncodeUint64(KeySQLText)
415+
return enc.EncodeString(expr)
416+
})
417+
}
418+
383419
// KeyValueBind is a type for encoding named SQL parameters
384420
type KeyValueBind struct {
385421
Key string

response.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Response struct {
1414
Data []interface{}
1515
MetaData []ColumnMetaData
1616
SQLInfo SQLInfo
17+
StmtID uint64
1718
buf smallBuf
1819
}
1920

@@ -178,6 +179,10 @@ func (resp *Response) decodeBody() (err error) {
178179
if err = d.Decode(&resp.MetaData); err != nil {
179180
return err
180181
}
182+
case KeyStmtID:
183+
if resp.StmtID, err = d.DecodeUint64(); err != nil {
184+
return err
185+
}
181186
default:
182187
if err = d.Skip(); err != nil {
183188
return err

tarantool_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,47 @@ func BenchmarkClientSerialSQL(b *testing.B) {
150150
}
151151
}
152152

153+
func BenchmarkClientSerialSQLPrepared(b *testing.B) {
154+
// Tarantool supports SQL since version 2.0.0
155+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
156+
if err != nil {
157+
b.Fatal("Could not check the Tarantool version")
158+
}
159+
if isLess {
160+
b.Skip()
161+
}
162+
163+
conn, err := Connect(server, opts)
164+
if err != nil {
165+
b.Errorf("Failed to connect: %s", err)
166+
return
167+
}
168+
defer conn.Close()
169+
170+
spaceNo := 519
171+
_, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})
172+
if err != nil {
173+
b.Errorf("Failed to replace: %s", err)
174+
}
175+
176+
res, err := conn.Prepare("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?")
177+
if err != nil {
178+
b.Fatalf("failed to prepare statement: %s", err)
179+
}
180+
if res.Code != 0 {
181+
b.Fatalf("failed to prepare statement: %s", err)
182+
}
183+
184+
b.ResetTimer()
185+
for i := 0; i < b.N; i++ {
186+
_, err := conn.Execute(res.StmtID, []interface{}{uint(1111)})
187+
if err != nil {
188+
b.Errorf("Select failed: %s", err.Error())
189+
break
190+
}
191+
}
192+
}
193+
153194
func BenchmarkClientFuture(b *testing.B) {
154195
var err error
155196

@@ -483,6 +524,49 @@ func BenchmarkClientParallelSQL(b *testing.B) {
483524
})
484525
}
485526

527+
func BenchmarkClientParallelSQLPrepared(b *testing.B) {
528+
// Tarantool supports SQL since version 2.0.0
529+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
530+
if err != nil {
531+
b.Fatal("Could not check the Tarantool version")
532+
}
533+
if isLess {
534+
b.Skip()
535+
}
536+
537+
conn, err := Connect(server, opts)
538+
if err != nil {
539+
b.Errorf("No connection available")
540+
return
541+
}
542+
defer conn.Close()
543+
544+
spaceNo := 519
545+
_, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})
546+
if err != nil {
547+
b.Errorf("No connection available")
548+
}
549+
550+
res, err := conn.Prepare("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?")
551+
if err != nil {
552+
b.Fatalf("failed to prepare statement: %s", err)
553+
}
554+
if res.Code != 0 {
555+
b.Fatalf("failed to prepare statement: %s", err)
556+
}
557+
558+
b.ResetTimer()
559+
b.RunParallel(func(pb *testing.PB) {
560+
for pb.Next() {
561+
_, err := conn.Execute(res.StmtID, []interface{}{uint(1111)})
562+
if err != nil {
563+
b.Errorf("Select failed: %s", err.Error())
564+
break
565+
}
566+
}
567+
})
568+
}
569+
486570
///////////////////
487571

488572
func TestClient(t *testing.T) {
@@ -1271,6 +1355,41 @@ func TestStressSQL(t *testing.T) {
12711355
}
12721356
}
12731357

1358+
func TestPreparedStmt(t *testing.T) {
1359+
// Tarantool supports SQL since version 2.0.0
1360+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
1361+
if err != nil {
1362+
t.Fatal("Could not check the Tarantool version")
1363+
}
1364+
if isLess {
1365+
t.Skip()
1366+
}
1367+
1368+
var conn *Connection
1369+
1370+
conn, err = Connect(server, opts)
1371+
if err != nil {
1372+
t.Fatalf("Failed to connect: %s", err.Error())
1373+
}
1374+
if conn == nil {
1375+
t.Fatal("conn is nil after Connect")
1376+
}
1377+
defer conn.Close()
1378+
1379+
res, err := conn.Prepare(selectPosQuery2)
1380+
if err != nil {
1381+
t.Fatalf("failed to prepare request: %s", err)
1382+
}
1383+
1384+
res2, err := conn.Execute(res.StmtID, []interface{}{1, "test"})
1385+
if err != nil {
1386+
t.Fatalf("failed to execute with prepared statement: %s", err)
1387+
}
1388+
if res2.Code != 0 {
1389+
t.Fatalf("Failed to execute: code error %d", res.Code)
1390+
}
1391+
}
1392+
12741393
func TestSchema(t *testing.T) {
12751394
var err error
12761395
var conn *Connection

0 commit comments

Comments
 (0)