Skip to content

Commit e2d11e0

Browse files
committed
sql: add support of prepared statements
This patch adds the support of prepared statements. Changed the API of method Execute, the first argument may be either string or uint64. Added new connectors interface methods in connector.go. Added new IPROTO-constants for support of prepared statements in const.go. Updated multi-package for corresponding to API of connector interface. Added Benchmarks for SQL-select prepared statement. Added examples of using Prepare in example_test.go. Fixed some grammar inconsistencies by method Execute. Updated CHANGELOG.md. Follows up #62 Closes #117
1 parent d44ffa0 commit e2d11e0

File tree

8 files changed

+239
-7
lines changed

8 files changed

+239
-7
lines changed

CHANGELOG.md

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

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

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

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

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

+40-4
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

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

+119
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,49 @@ func BenchmarkSQLParallel(b *testing.B) {
450450
})
451451
}
452452

453+
func BenchmarkSQLPreparedParallel(b *testing.B) {
454+
// Tarantool supports SQL since version 2.0.0
455+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
456+
if err != nil {
457+
b.Fatal("Could not check the Tarantool version")
458+
}
459+
if isLess {
460+
b.Skip()
461+
}
462+
463+
conn, err := Connect(server, opts)
464+
if err != nil {
465+
b.Errorf("No connection available")
466+
return
467+
}
468+
defer conn.Close()
469+
470+
spaceNo := 519
471+
_, err = conn.Replace(spaceNo, []interface{}{uint(1111), "hello", "world"})
472+
if err != nil {
473+
b.Errorf("No connection available")
474+
}
475+
476+
res, err := conn.Prepare("SELECT NAME0,NAME1,NAME2 FROM SQL_TEST WHERE NAME0=?")
477+
if err != nil {
478+
b.Fatalf("failed to prepare statement: %s", err)
479+
}
480+
if res.Code != 0 {
481+
b.Fatalf("failed to prepare statement: %s", err)
482+
}
483+
484+
b.ResetTimer()
485+
b.RunParallel(func(pb *testing.PB) {
486+
for pb.Next() {
487+
_, err := conn.Execute(res.StmtID, []interface{}{uint(1111)})
488+
if err != nil {
489+
b.Errorf("Select failed: %s", err.Error())
490+
break
491+
}
492+
}
493+
})
494+
}
495+
453496
func BenchmarkSQLSerial(b *testing.B) {
454497
// Tarantool supports SQL since version 2.0.0
455498
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
@@ -483,6 +526,47 @@ func BenchmarkSQLSerial(b *testing.B) {
483526
}
484527
}
485528

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

488572
func TestClient(t *testing.T) {
@@ -1001,6 +1085,41 @@ func TestSQL(t *testing.T) {
10011085
}
10021086
}
10031087

1088+
func TestPreparedStmt(t *testing.T) {
1089+
// Tarantool supports SQL since version 2.0.0
1090+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)
1091+
if err != nil {
1092+
t.Fatal("Could not check the Tarantool version")
1093+
}
1094+
if isLess {
1095+
t.Skip()
1096+
}
1097+
1098+
var conn *Connection
1099+
1100+
conn, err = Connect(server, opts)
1101+
if err != nil {
1102+
t.Fatalf("Failed to connect: %s", err.Error())
1103+
}
1104+
if conn == nil {
1105+
t.Fatal("conn is nil after Connect")
1106+
}
1107+
defer conn.Close()
1108+
1109+
res, err := conn.Prepare(selectPosQuery2)
1110+
if err != nil {
1111+
t.Fatalf("failed to prepare request: %s", err)
1112+
}
1113+
1114+
res2, err := conn.Execute(res.StmtID, []interface{}{1, "test"})
1115+
if err != nil {
1116+
t.Fatalf("failed to execute with prepared statement: %s", err)
1117+
}
1118+
if res2.Code != 0 {
1119+
t.Fatalf("Failed to execute: code error %d", res.Code)
1120+
}
1121+
}
1122+
10041123
func TestSQLTyped(t *testing.T) {
10051124
// Tarantool supports SQL since version 2.0.0
10061125
isLess, err := test_helpers.IsTarantoolVersionLess(2, 0, 0)

0 commit comments

Comments
 (0)