Skip to content

Commit c92e8cc

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 9fb2337 commit c92e8cc

15 files changed

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

config.lua

-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ local function push_func(cnt)
117117
return cnt
118118
end
119119
rawset(_G, 'push_func', push_func)
120-
121120
box.space.test:truncate()
122121

123122
--box.schema.user.revoke('guest', 'read,write,execute', 'universe')

connection.go

+10
Original file line numberDiff line numberDiff line change
@@ -1009,3 +1009,13 @@ func (conn *Connection) OverrideSchema(s *Schema) {
10091009
conn.Schema = s
10101010
}
10111011
}
1012+
1013+
// NewPrepareStatement passes a sql statement to Tarantool for preparation synchronously.
1014+
func (conn *Connection) NewPrepareStatement(expr string) (*PreparedStatement, error) {
1015+
req := NewPrepareRequest(expr)
1016+
resp, err := conn.Do(req).Get()
1017+
if err != nil {
1018+
return nil, err
1019+
}
1020+
return newPreparedStatement(conn, resp), nil
1021+
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,9 @@ 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.StickyRequest); ok {
530+
return stickyReq.Conn().Do(req)
531+
}
529532
conn, err := connPool.getNextConnection(userMode)
530533
if err != nil {
531534
return newErrorFuture(err)
@@ -788,3 +791,12 @@ func newErrorFuture(err error) *tarantool.Future {
788791
fut.SetError(err)
789792
return fut
790793
}
794+
795+
// NewPrepareStatement passes a sql statement to Tarantool for preparation synchronously.
796+
func (connPool *ConnectionPool) NewPrepareStatement(expr string, userMode Mode) (*tarantool.PreparedStatement, error) {
797+
conn, err := connPool.getNextConnection(userMode)
798+
if err != nil {
799+
return nil, err
800+
}
801+
return conn.NewPrepareStatement(expr)
802+
}

connection_pool/connection_pool_test.go

+69
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 TestNewPrepareStatement(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.NewPrepareStatement("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

+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_NewPrepareStatement() {
553+
pool, err := examplePool(testRoles)
554+
if err != nil {
555+
fmt.Println(err)
556+
}
557+
defer pool.Close()
558+
559+
stmt, err := pool.NewPrepareStatement("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+
}

connector.go

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Connector interface {
3030
Call16Typed(functionName string, args interface{}, result interface{}) (err error)
3131
Call17Typed(functionName string, args interface{}, result interface{}) (err error)
3232
EvalTyped(expr string, args interface{}, result interface{}) (err error)
33+
ExecuteTyped(expr string, args interface{}, result interface{}) (SQLInfo, []ColumnMetaData, error)
3334

3435
SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *Future
3536
InsertAsync(space interface{}, tuple interface{}) *Future
@@ -41,6 +42,7 @@ type Connector interface {
4142
Call16Async(functionName string, args interface{}) *Future
4243
Call17Async(functionName string, args interface{}) *Future
4344
EvalAsync(expr string, args interface{}) *Future
45+
ExecuteAsync(expr string, args interface{}) *Future
4446

4547
Do(req Request) (fut *Future)
4648
}

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

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 Prepare.
656+
func ExampleConnection_Do() {
657+
// Tarantool supports SQL since version 2.0.0
658+
isLess, _ := test_helpers.IsTarantoolVersionLess(2, 0, 0)
659+
if 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.NewPrepareStatement("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+
}

future.go

+13
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,16 @@ func (fut *Future) Err() error {
239239
fut.wait()
240240
return fut.err
241241
}
242+
243+
func (fut *Future) GetPreparedStatement(conn *Connection) (*PreparedStatement, error) {
244+
resp, err := fut.Get()
245+
if err != nil {
246+
return nil, err
247+
}
248+
return &PreparedStatement{
249+
StatementID: PreparedStatementID(resp.StmtID),
250+
ParamCount: resp.BindCount,
251+
Conn: conn,
252+
MetaData: resp.MetaData,
253+
}, nil
254+
}

multi/multi.go

+15
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,11 @@ func (connMulti *ConnectionMulti) EvalTyped(expr string, args interface{}, resul
419419
return connMulti.getCurrentConnection().EvalTyped(expr, args, result)
420420
}
421421

422+
// ExecuteTyped passes sql expression to Tarantool for execution.
423+
func (connMulti *ConnectionMulti) ExecuteTyped(expr string, args interface{}, result interface{}) (tarantool.SQLInfo, []tarantool.ColumnMetaData, error) {
424+
return connMulti.getCurrentConnection().ExecuteTyped(expr, args, result)
425+
}
426+
422427
// SelectAsync sends select request to Tarantool and returns Future.
423428
func (connMulti *ConnectionMulti) SelectAsync(space, index interface{}, offset, limit, iterator uint32, key interface{}) *tarantool.Future {
424429
return connMulti.getCurrentConnection().SelectAsync(space, index, offset, limit, iterator, key)
@@ -482,6 +487,16 @@ func (connMulti *ConnectionMulti) EvalAsync(expr string, args interface{}) *tara
482487
return connMulti.getCurrentConnection().EvalAsync(expr, args)
483488
}
484489

490+
// ExecuteAsync passes sql expression to Tarantool for execution.
491+
func (connMulti *ConnectionMulti) ExecuteAsync(expr string, args interface{}) *tarantool.Future {
492+
return connMulti.getCurrentConnection().ExecuteAsync(expr, args)
493+
}
494+
495+
// NewPrepareStatement passes a sql statement to Tarantool for preparation synchronously.
496+
func (connMulti *ConnectionMulti) NewPrepareStatement(expr string) (*tarantool.PreparedStatement, error) {
497+
return connMulti.getCurrentConnection().NewPrepareStatement(expr)
498+
}
499+
485500
// Do sends the request and returns a future.
486501
func (connMulti *ConnectionMulti) Do(req tarantool.Request) *tarantool.Future {
487502
return connMulti.getCurrentConnection().Do(req)

0 commit comments

Comments
 (0)