From 150b1fa47e903247fee6ddb5afb9a53d14c6e0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Tremblay?= Date: Tue, 20 Jul 2021 17:22:04 -0700 Subject: [PATCH 1/3] basic support for prepared statements --- README.md | 18 +++++++++ api.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ write.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+) diff --git a/README.md b/README.md index 892293a..f8afbb2 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,24 @@ gorqlite.TraceOn(os.Stderr) // turn off gorqlite.TraceOff() + + +// using prepared statements +wr, err := conn.WritePrepared( + []*gorqlite.PreparedStatement{ + { + Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", + Arguments: []interface{}{7, "James Bond", []byte{0x42}} + } + } +) +// alternatively +wr, err := conn.WriteOnePrepared( + &gorqlite.PreparedStatement{ + Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", + Arguments: []interface{}{7, "James Bond", []byte{0x42}}, + }, +) ``` ## Important Notes diff --git a/api.go b/api.go index 54937e0..ce0cc5a 100644 --- a/api.go +++ b/api.go @@ -21,6 +21,11 @@ import "io/ioutil" import "net/http" import "time" +type PreparedStatement struct { + Query string + Arguments []interface{} +} + /* ***************************************************************** method: rqliteApiGet() - for api_STATUS @@ -201,3 +206,105 @@ PeerLoop: } return responseBody, errors.New(stringBuffer.String()) } + + +func (conn *Connection) rqliteApiPostPrepared(apiOp apiOperation, sqlStatements []*PreparedStatement) ([]byte, error) { + var responseBody []byte + + switch apiOp { + case api_QUERY: + trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) + case api_WRITE: + trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) + default: + return responseBody, errors.New("weird! called for an invalid apiOperation in rqliteApiPost()") + } + + // jsonify the statements. not really needed in the + // case of api_STATUS but doesn't hurt + + + formattedStatements := make([][]interface{}, 0, len(sqlStatements)) + + for _, statement := range sqlStatements { + formattedStatement := make([]interface{}, 0, len(statement.Arguments)+1) + formattedStatement = append(formattedStatement, statement.Query) + + for _, argument := range statement.Arguments { + formattedStatement = append(formattedStatement, argument) + } + formattedStatements = append(formattedStatements, formattedStatement) + } + + jStatements, err := json.Marshal(formattedStatements) + if err != nil { + return nil, err + } + + // just to be safe, check this + peersToTry := conn.cluster.makePeerList() + if len(peersToTry) < 1 { + return responseBody, errors.New("I don't have any cluster info") + } + + // failure log is used so that if all peers fail, we can say something + // about why each failed + failureLog := make([]string, 0) + +PeerLoop: + for peerNum, peer := range peersToTry { + trace("%s: trying peer #%d", conn.ID, peerNum) + + // we're doing a post, and the RFCs say that if you get a 301, it's not + // automatically followed, so we have to do that ourselves + + responseStatus := "Haven't Tried Yet" + var url string + for responseStatus == "Haven't Tried Yet" || responseStatus == "301 Moved Permanently" { + url = conn.assembleURL(apiOp, peer) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jStatements)) + if err != nil { + trace("%s: got error '%s' doing http.NewRequest", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + trace("%s: got error '%s' doing client.Do", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + defer response.Body.Close() + responseBody, err = ioutil.ReadAll(response.Body) + if err != nil { + trace("%s: got error '%s' doing ioutil.ReadAll", conn.ID, err.Error()) + failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) + continue PeerLoop + } + responseStatus = response.Status + if responseStatus == "301 Moved Permanently" { + v := response.Header["Location"] + failureLog = append(failureLog, fmt.Sprintf("%s redirected me to %s", url, v[0])) + url = v[0] + continue PeerLoop + } else if responseStatus == "200 OK" { + trace("%s: api call OK, returning", conn.ID) + return responseBody, nil + } else { + trace("%s: got error in responseStatus: %s", conn.ID, responseStatus) + failureLog = append(failureLog, fmt.Sprintf("%s failed, got: %s", url, response.Status)) + continue PeerLoop + } + } + } + + // if we got here, all peers failed. Let's build a verbose error message + var stringBuffer bytes.Buffer + stringBuffer.WriteString("tried all peers unsuccessfully. here are the results:\n") + for n, v := range failureLog { + stringBuffer.WriteString(fmt.Sprintf(" peer #%d: %s\n", n, v)) + } + return responseBody, errors.New(stringBuffer.String()) +} \ No newline at end of file diff --git a/write.go b/write.go index 5716927..153ea4d 100644 --- a/write.go +++ b/write.go @@ -70,6 +70,16 @@ func (conn *Connection) WriteOne(sqlStatement string) (wr WriteResult, err error return wra[0], err } +func (conn *Connection) WriteOnePrepared(statement *PreparedStatement) (wr WriteResult, err error) { + if conn.hasBeenClosed { + wr.Err = errClosed + return wr, errClosed + } + wra, err := conn.WritePrepared([]*PreparedStatement{statement}) + return wra[0], err +} + + /* Write() is used to perform DDL/DML in the database. ALTER, CREATE, DELETE, DROP, INSERT, UPDATE, etc. all go through Write(). @@ -171,6 +181,98 @@ func (conn *Connection) Write(sqlStatements []string) (results []WriteResult, er } } +func (conn *Connection) WritePrepared(sqlStatements []*PreparedStatement) (results []WriteResult, err error) { + results = make([]WriteResult, 0) + + if conn.hasBeenClosed { + var errResult WriteResult + errResult.Err = errClosed + results = append(results, errResult) + return results, errClosed + } + + trace("%s: Write() for %d statements", conn.ID, len(sqlStatements)) + + response, err := conn.rqliteApiPostPrepared(api_WRITE, sqlStatements) + if err != nil { + trace("%s: rqliteApiCall() ERROR: %s", conn.ID, err.Error()) + var errResult WriteResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + trace("%s: rqliteApiCall() OK", conn.ID) + + var sections map[string]interface{} + err = json.Unmarshal(response, §ions) + if err != nil { + trace("%s: json.Unmarshal() ERROR: %s", conn.ID, err.Error()) + var errResult WriteResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + + /* + at this point, we have a "results" section and + a "time" section. we can igore the latter. + */ + + resultsArray, ok := sections["results"].([]interface{}) + if !ok { + err = errors.New("Result key is missing from response") + trace("%s: sections[\"results\"] ERROR: %s", conn.ID, err) + var errResult WriteResult + errResult.Err = err + results = append(results, errResult) + return results, err + } + trace("%s: I have %d result(s) to parse", conn.ID, len(resultsArray)) + numStatementErrors := 0 + for n, k := range resultsArray { + trace("%s: starting on result %d", conn.ID, n) + thisResult := k.(map[string]interface{}) + + var thisWR WriteResult + thisWR.conn = conn + + // did we get an error? + _, ok := thisResult["error"] + if ok { + trace("%s: have an error on this result: %s", conn.ID, thisResult["error"].(string)) + thisWR.Err = errors.New(thisResult["error"].(string)) + results = append(results, thisWR) + numStatementErrors += 1 + continue + } + + _, ok = thisResult["last_insert_id"] + if ok { + thisWR.LastInsertID = int64(thisResult["last_insert_id"].(float64)) + } + + _, ok = thisResult["rows_affected"] // could be zero for a CREATE + if ok { + thisWR.RowsAffected = int64(thisResult["rows_affected"].(float64)) + } + _, ok = thisResult["time"] // could be nil + if ok { + thisWR.Timing = thisResult["time"].(float64) + } + + trace("%s: this result (LII,RA,T): %d %d %f", conn.ID, thisWR.LastInsertID, thisWR.RowsAffected, thisWR.Timing) + results = append(results, thisWR) + } + + trace("%s: finished parsing, returning %d results", conn.ID, len(results)) + + if numStatementErrors > 0 { + return results, errors.New(fmt.Sprintf("there were %d statement errors", numStatementErrors)) + } else { + return results, nil + } +} + /* ***************************************************************** type: WriteResult From 54f684987bec9b12621073667f6c78f75923d0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Tremblay?= Date: Tue, 20 Jul 2021 17:23:37 -0700 Subject: [PATCH 2/3] fix tabulation in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f8afbb2..d83eeda 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ wr, err := conn.WritePrepared( { Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", Arguments: []interface{}{7, "James Bond", []byte{0x42}} - } + } } ) // alternatively @@ -152,7 +152,7 @@ wr, err := conn.WriteOnePrepared( &gorqlite.PreparedStatement{ Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", Arguments: []interface{}{7, "James Bond", []byte{0x42}}, - }, + }, ) ``` ## Important Notes From cf949a27812050f2c7cb4b725b7ecfc3b2db3d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Tremblay?= Date: Tue, 20 Jul 2021 18:51:18 -0700 Subject: [PATCH 3/3] fix comments and add tests --- README.md | 8 ++-- api.go | 97 ++-------------------------------------------- query.go | 31 +++++++++++++-- query_test.go | 31 +++++++++++++-- write.go | 105 ++++++++++---------------------------------------- write_test.go | 54 +++++++++++++++++++++++++- 6 files changed, 137 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index d83eeda..4d891d0 100644 --- a/README.md +++ b/README.md @@ -142,16 +142,16 @@ gorqlite.TraceOff() wr, err := conn.WritePrepared( []*gorqlite.PreparedStatement{ { - Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", - Arguments: []interface{}{7, "James Bond", []byte{0x42}} + Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", + Arguments: []interface{}{7, "James Bond", []byte{0x42}} } } ) // alternatively wr, err := conn.WriteOnePrepared( &gorqlite.PreparedStatement{ - Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", - Arguments: []interface{}{7, "James Bond", []byte{0x42}}, + Query: "INSERT INTO secret_agents(id, name, secret) VALUES(?, ?, ?)", + Arguments: []interface{}{7, "James Bond", []byte{0x42}}, }, ) ``` diff --git a/api.go b/api.go index ce0cc5a..becd967 100644 --- a/api.go +++ b/api.go @@ -4,7 +4,7 @@ package gorqlite this file has low level stuff: rqliteApiGet() - rqliteApiPost() + rqliteApiPostPrepared() There is some code duplication between those and they should probably be combined into one function. @@ -107,7 +107,7 @@ PeerLoop: /* ***************************************************************** - method: rqliteApiPost() - for api_QUERY and api_WRITE + method: rqliteApiPostPrepared() - for api_QUERY and api_WRITE - lowest level interface - does not do any JSON unmarshaling - handles 301s, etc. @@ -119,95 +119,6 @@ PeerLoop: * *****************************************************************/ -func (conn *Connection) rqliteApiPost(apiOp apiOperation, sqlStatements []string) ([]byte, error) { - var responseBody []byte - - switch apiOp { - case api_QUERY: - trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) - case api_WRITE: - trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) - default: - return responseBody, errors.New("weird! called for an invalid apiOperation in rqliteApiPost()") - } - - // jsonify the statements. not really needed in the - // case of api_STATUS but doesn't hurt - - jStatements, err := json.Marshal(sqlStatements) - if err != nil { - return nil, err - } - - // just to be safe, check this - peersToTry := conn.cluster.makePeerList() - if len(peersToTry) < 1 { - return responseBody, errors.New("I don't have any cluster info") - } - - // failure log is used so that if all peers fail, we can say something - // about why each failed - failureLog := make([]string, 0) - -PeerLoop: - for peerNum, peer := range peersToTry { - trace("%s: trying peer #%d", conn.ID, peerNum) - - // we're doing a post, and the RFCs say that if you get a 301, it's not - // automatically followed, so we have to do that ourselves - - responseStatus := "Haven't Tried Yet" - var url string - for responseStatus == "Haven't Tried Yet" || responseStatus == "301 Moved Permanently" { - url = conn.assembleURL(apiOp, peer) - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jStatements)) - if err != nil { - trace("%s: got error '%s' doing http.NewRequest", conn.ID, err.Error()) - failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) - continue PeerLoop - } - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - response, err := client.Do(req) - if err != nil { - trace("%s: got error '%s' doing client.Do", conn.ID, err.Error()) - failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) - continue PeerLoop - } - defer response.Body.Close() - responseBody, err = ioutil.ReadAll(response.Body) - if err != nil { - trace("%s: got error '%s' doing ioutil.ReadAll", conn.ID, err.Error()) - failureLog = append(failureLog, fmt.Sprintf("%s failed due to %s", url, err.Error())) - continue PeerLoop - } - responseStatus = response.Status - if responseStatus == "301 Moved Permanently" { - v := response.Header["Location"] - failureLog = append(failureLog, fmt.Sprintf("%s redirected me to %s", url, v[0])) - url = v[0] - continue PeerLoop - } else if responseStatus == "200 OK" { - trace("%s: api call OK, returning", conn.ID) - return responseBody, nil - } else { - trace("%s: got error in responseStatus: %s", conn.ID, responseStatus) - failureLog = append(failureLog, fmt.Sprintf("%s failed, got: %s", url, response.Status)) - continue PeerLoop - } - } - } - - // if we got here, all peers failed. Let's build a verbose error message - var stringBuffer bytes.Buffer - stringBuffer.WriteString("tried all peers unsuccessfully. here are the results:\n") - for n, v := range failureLog { - stringBuffer.WriteString(fmt.Sprintf(" peer #%d: %s\n", n, v)) - } - return responseBody, errors.New(stringBuffer.String()) -} - - func (conn *Connection) rqliteApiPostPrepared(apiOp apiOperation, sqlStatements []*PreparedStatement) ([]byte, error) { var responseBody []byte @@ -217,7 +128,7 @@ func (conn *Connection) rqliteApiPostPrepared(apiOp apiOperation, sqlStatements case api_WRITE: trace("%s: rqliteApiGet() post called for a QUERY of %d statements", conn.ID, len(sqlStatements)) default: - return responseBody, errors.New("weird! called for an invalid apiOperation in rqliteApiPost()") + return responseBody, errors.New("weird! called for an invalid apiOperation in rqliteApiPostPrepared()") } // jsonify the statements. not really needed in the @@ -229,7 +140,7 @@ func (conn *Connection) rqliteApiPostPrepared(apiOp apiOperation, sqlStatements for _, statement := range sqlStatements { formattedStatement := make([]interface{}, 0, len(statement.Arguments)+1) formattedStatement = append(formattedStatement, statement.Query) - + for _, argument := range statement.Arguments { formattedStatement = append(formattedStatement, argument) } diff --git a/query.go b/query.go index 11b2a34..b43cfcd 100644 --- a/query.go +++ b/query.go @@ -105,12 +105,21 @@ func (conn *Connection) QueryOne(sqlStatement string) (qr QueryResult, err error return qra[0], err } +func (conn *Connection) QueryOnePrepared(statement *PreparedStatement) (qr QueryResult, err error) { + if conn.hasBeenClosed { + qr.Err = errClosed + return qr, errClosed + } + qra, err := conn.QueryPrepared([]*PreparedStatement{statement}) + return qra[0], err +} + /* -Query() is used to perform SELECT operations in the database. +QueryPrepared() is used to perform SELECT operations in the database. It takes an array of SQL statements and executes them in a single transaction, returning an array of QueryResult vars. */ -func (conn *Connection) Query(sqlStatements []string) (results []QueryResult, err error) { +func (conn *Connection) QueryPrepared(sqlStatements []*PreparedStatement) (results []QueryResult, err error) { results = make([]QueryResult, 0) if conn.hasBeenClosed { @@ -122,7 +131,7 @@ func (conn *Connection) Query(sqlStatements []string) (results []QueryResult, er trace("%s: Query() for %d statements", conn.ID, len(sqlStatements)) // if we get an error POSTing, that's a showstopper - response, err := conn.rqliteApiPost(api_QUERY, sqlStatements) + response, err := conn.rqliteApiPostPrepared(api_QUERY, sqlStatements) if err != nil { trace("%s: rqliteApiCall() ERROR: %s", conn.ID, err.Error()) var errResult QueryResult @@ -203,6 +212,22 @@ func (conn *Connection) Query(sqlStatements []string) (results []QueryResult, er } } +/* +Query() is used to perform SELECT operations in the database. + +It takes an array of SQL statements and executes them in a single transaction, returning an array of QueryResult vars. +*/ + +func (conn *Connection) Query(sqlStatements []string) (results []QueryResult, err error) { + preparedStatements := make([]*PreparedStatement, 0, len(sqlStatements)) + for _, sqlStatement := range sqlStatements { + preparedStatements = append(preparedStatements, &PreparedStatement{ + Query: sqlStatement, + }) + } + return conn.QueryPrepared(preparedStatements) +} + /* ***************************************************************** type: QueryResult diff --git a/query_test.go b/query_test.go index 055fd1c..de9746e 100644 --- a/query_test.go +++ b/query_test.go @@ -10,7 +10,6 @@ func TestQueryOne(t *testing.T) { var wr WriteResult var qr QueryResult var wResults []WriteResult - var qResults []QueryResult var err error t.Logf("trying Open") @@ -61,6 +60,19 @@ func TestQueryOne(t *testing.T) { t.Fail() } + t.Logf("trying QueryOnePrepared") + qr, err = conn.QueryOnePrepared( + &PreparedStatement{ + Query: fmt.Sprintf("SELECT name, ts FROM %s WHERE id > ?", testTableName()), + Arguments: []interface{}{3}, + }, + ) + + if err != nil { + t.Logf("--> FAILED") + t.Fail() + } + t.Logf("trying Next()") na := qr.Next() if na != true { @@ -171,11 +183,24 @@ func TestQueryOne(t *testing.T) { t2 = append(t2, "SELECT id FROM "+testTableName()+"") t2 = append(t2, "SELECT name FROM "+testTableName()+"") t2 = append(t2, "SELECT id,name FROM "+testTableName()+"") - qResults, err = conn.Query(t2) + _, err = conn.Query(t2) + if err == nil { + t.Logf("--> FAILED") + t.Fail() + } + + t.Logf("trying Query after Close") + _, err = conn.QueryPrepared( + []*PreparedStatement{ + { Query: fmt.Sprintf("SELECT id FROM %s", testTableName()), }, + { Query: fmt.Sprintf("SELECT name FROM %s", testTableName()), }, + { Query: fmt.Sprintf("SELECT id, name FROM %s", testTableName()), }, + }, + ) + if err == nil { t.Logf("--> FAILED") t.Fail() } - _ = qResults } diff --git a/write.go b/write.go index 153ea4d..356b32f 100644 --- a/write.go +++ b/write.go @@ -70,6 +70,11 @@ func (conn *Connection) WriteOne(sqlStatement string) (wr WriteResult, err error return wra[0], err } +/* +WriteOnePrepared() is a convenience method that wraps WritePrepared() into a single-statement +method. +*/ + func (conn *Connection) WriteOnePrepared(statement *PreparedStatement) (wr WriteResult, err error) { if conn.hasBeenClosed { wr.Err = errClosed @@ -87,99 +92,29 @@ Write() takes an array of SQL statements, and returns an equal-sized array of Wr All statements are executed as a single transaction. +Write() uses WritePrepared() + Write() returns an error if one is encountered during its operation. If it's something like a call to the rqlite API, then it'll return that error. If one statement out of several has an error, it will return a generic "there were %d statement errors" and you'll have to look at the individual statement's Err for more info. */ func (conn *Connection) Write(sqlStatements []string) (results []WriteResult, err error) { - results = make([]WriteResult, 0) - - if conn.hasBeenClosed { - var errResult WriteResult - errResult.Err = errClosed - results = append(results, errResult) - return results, errClosed + preparedStatements := make([]*PreparedStatement, 0, len(sqlStatements)) + for _, sqlStatement := range sqlStatements { + preparedStatements = append(preparedStatements, &PreparedStatement{ + Query: sqlStatement, + }) } + return conn.WritePrepared(preparedStatements) +} - trace("%s: Write() for %d statements", conn.ID, len(sqlStatements)) - - response, err := conn.rqliteApiPost(api_WRITE, sqlStatements) - if err != nil { - trace("%s: rqliteApiCall() ERROR: %s", conn.ID, err.Error()) - var errResult WriteResult - errResult.Err = err - results = append(results, errResult) - return results, err - } - trace("%s: rqliteApiCall() OK", conn.ID) - - var sections map[string]interface{} - err = json.Unmarshal(response, §ions) - if err != nil { - trace("%s: json.Unmarshal() ERROR: %s", conn.ID, err.Error()) - var errResult WriteResult - errResult.Err = err - results = append(results, errResult) - return results, err - } - - /* - at this point, we have a "results" section and - a "time" section. we can igore the latter. - */ - - resultsArray, ok := sections["results"].([]interface{}) - if !ok { - err = errors.New("Result key is missing from response") - trace("%s: sections[\"results\"] ERROR: %s", conn.ID, err) - var errResult WriteResult - errResult.Err = err - results = append(results, errResult) - return results, err - } - trace("%s: I have %d result(s) to parse", conn.ID, len(resultsArray)) - numStatementErrors := 0 - for n, k := range resultsArray { - trace("%s: starting on result %d", conn.ID, n) - thisResult := k.(map[string]interface{}) - - var thisWR WriteResult - thisWR.conn = conn - - // did we get an error? - _, ok := thisResult["error"] - if ok { - trace("%s: have an error on this result: %s", conn.ID, thisResult["error"].(string)) - thisWR.Err = errors.New(thisResult["error"].(string)) - results = append(results, thisWR) - numStatementErrors += 1 - continue - } - - _, ok = thisResult["last_insert_id"] - if ok { - thisWR.LastInsertID = int64(thisResult["last_insert_id"].(float64)) - } - - _, ok = thisResult["rows_affected"] // could be zero for a CREATE - if ok { - thisWR.RowsAffected = int64(thisResult["rows_affected"].(float64)) - } - _, ok = thisResult["time"] // could be nil - if ok { - thisWR.Timing = thisResult["time"].(float64) - } +/* +WritePrepared() is used to perform DDL/DML in the database. ALTER, CREATE, DELETE, DROP, INSERT, UPDATE, etc. all go through Write(). - trace("%s: this result (LII,RA,T): %d %d %f", conn.ID, thisWR.LastInsertID, thisWR.RowsAffected, thisWR.Timing) - results = append(results, thisWR) - } +WritePrepared() takes an array of SQL statements, and returns an equal-sized array of WriteResults, each corresponding to the SQL statement that produced it. - trace("%s: finished parsing, returning %d results", conn.ID, len(results)) +All statements are executed as a single transaction. - if numStatementErrors > 0 { - return results, errors.New(fmt.Sprintf("there were %d statement errors", numStatementErrors)) - } else { - return results, nil - } -} +WritePrepared() returns an error if one is encountered during its operation. If it's something like a call to the rqlite API, then it'll return that error. If one statement out of several has an error, it will return a generic "there were %d statement errors" and you'll have to look at the individual statement's Err for more info. +*/ func (conn *Connection) WritePrepared(sqlStatements []*PreparedStatement) (results []WriteResult, err error) { results = make([]WriteResult, 0) diff --git a/write_test.go b/write_test.go index 1adb63d..79fdd78 100644 --- a/write_test.go +++ b/write_test.go @@ -1,6 +1,9 @@ package gorqlite -import "testing" +import ( + "fmt" + "testing" +) // import "os" @@ -49,6 +52,25 @@ func TestWriteOne(t *testing.T) { t.Fail() } + t.Logf("trying WriteOne INSERT") + wr, err = conn.WriteOnePrepared( + &PreparedStatement{ + Query: fmt.Sprintf("INSERT INTO %s (id, name) VALUES (?, ?)", testTableName()), + Arguments: []interface{}{1, "aaa bbb ccc"}, + }, + ) + + if err != nil { + t.Logf("--> FAILED") + t.Fail() + } + + t.Logf("checking WriteOnePrepared RowsAffected") + if wr.RowsAffected != 1 { + t.Logf("--> FAILED") + t.Fail() + } + t.Logf("trying WriteOne DROP") wr, err = conn.WriteOne("DROP TABLE IF EXISTS " + testTableName() + "") if err != nil { @@ -96,6 +118,36 @@ func TestWrite(t *testing.T) { t.Fail() } + t.Logf("trying Write INSERT") + results, err = conn.WritePrepared( + []*PreparedStatement{ + { + Query: fmt.Sprintf("INSERT INTO %s (id, name) VALUES (?, ?)", testTableName()), + Arguments: []interface{}{1, "aaa bbb ccc"}, + }, + { + Query: fmt.Sprintf("INSERT INTO %s (id, name) VALUES (?, ?)", testTableName()), + Arguments: []interface{}{1, "aaa bbb ccc"}, + }, + { + Query: fmt.Sprintf("INSERT INTO %s (id, name) VALUES (?, ?)", testTableName()), + Arguments: []interface{}{1, "aaa bbb ccc"}, + }, + { + Query: fmt.Sprintf("INSERT INTO %s (id, name) VALUES (?, ?)", testTableName()), + Arguments: []interface{}{1, "aaa bbb ccc"}, + }, + }, + ) + if err != nil { + t.Logf("--> FAILED") + t.Fail() + } + if len(results) != 4 { + t.Logf("--> FAILED") + t.Fail() + } + t.Logf("trying Write DROP") s = make([]string, 0) s = append(s, "DROP TABLE IF EXISTS "+testTableName()+"")