diff --git a/README.md b/README.md index 0837317..1ee6b58 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,44 @@ func main() { - `UPDATE` - `DELETE` +## Transaction support + +`godynamo` supports transactions that consist of write statements (e.g. `INSERT`, `UPDATE` and `DELETE`) since [v0.2.0](RELEASE-NOTES.md). Please note the following: + +- Any limitation set by [DynamoDB/PartiQL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.multiplestatements.transactions.html) will apply. +- [Table](SQL_TABLE.md) and [Index](SQL_INDEX.md) statements are not supported. +- `UPDATE`/`DELETE` with `RETURNING` and `SELECT` statements are not supported. + +Example: +```go +tx, err := db.Begin() +if err != nil { + panic(err) +} +defer tx.Rollback() +result1, _ := tx.Exec(`INSERT INTO "tbltest" VALUE {'app': ?, 'user': ?, 'active': ?}`, "app0", "user1", true) +result2, _ := tx.Exec(`INSERT INTO "tbltest" VALUE {'app': ?, 'user': ?, 'duration': ?}`, "app0", "user2", 1.23) +err = tx.Commit() +if err != nil { + panic(err) +} +rowsAffected1, err1 := fmt.Println(result1.RowsAffected()) +if err1 != nil { + panic(err1) +} +fmt.Println("RowsAffected:", rowsAffected1) // output "RowsAffected: 1" + +rowsAffected2, err2 := fmt.Println(result2.RowsAffected()) +if err2 != nil { + panic(err2) +} +fmt.Println("RowsAffected:", rowsAffected2) // output "RowsAffected: 1" +``` + +> If a statement's condition check fails (e.g. deleting non-existing item), the whole transaction will also fail. This behaviour is different from executing statements in non-transactional mode where failed condition check results in `0` affected row without error. +> +> You can use [`EXISTS` function](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-functions.exists.html) for condition checking. + ## License MIT - See [LICENSE.md](LICENSE.md). diff --git a/stmt_document.go b/stmt_document.go index fdd759e..eee1738 100644 --- a/stmt_document.go +++ b/stmt_document.go @@ -101,6 +101,9 @@ func (s *StmtSelect) Query(values []driver.Value) (driver.Rows, error) { // @Available since v0.2.0 func (s *StmtSelect) QueryContext(ctx context.Context, values []driver.NamedValue) (driver.Rows, error) { outputFn, err := s.conn.executeContext(ctx, s.Stmt, values) + if err == ErrInTx { + return &TxResultResultSet{wrap: ResultResultSet{err: err}, outputFn: outputFn}, nil + } result := &ResultResultSet{stmtOutput: outputFn()} if err == nil { result.init() diff --git a/tx.go b/tx.go index abbf6e3..e8295c2 100644 --- a/tx.go +++ b/tx.go @@ -1,7 +1,9 @@ package godynamo import ( + "database/sql/driver" "fmt" + "reflect" ) // TxResultNoResultSet is transaction-aware version of ResultNoResultSet. @@ -33,6 +35,62 @@ func (t *TxResultNoResultSet) RowsAffected() (int64, error) { return t.affectedRows, nil } +// TxResultResultSet is transaction-aware version of ResultResultSet. +// +// @Available since v0.2.0 +type TxResultResultSet struct { + wrap ResultResultSet + hasOutput bool + outputFn executeStatementOutputWrapper +} + +func (r *TxResultResultSet) checkOutput() { + if !r.hasOutput { + r.wrap.stmtOutput = r.outputFn() + fmt.Println("DEBUG", r.wrap.stmtOutput) + if r.wrap.stmtOutput != nil { + r.wrap.err = nil + r.hasOutput = true + r.wrap.init() + } + } +} + +// Columns implements driver.Rows/Columns. +func (r *TxResultResultSet) Columns() []string { + r.checkOutput() + return r.wrap.Columns() +} + +// ColumnTypeScanType implements driver.RowsColumnTypeScanType/ColumnTypeScanType +func (r *TxResultResultSet) ColumnTypeScanType(index int) reflect.Type { + r.checkOutput() + return r.wrap.ColumnTypeScanType(index) +} + +// ColumnTypeDatabaseTypeName implements driver.RowsColumnTypeDatabaseTypeName/ColumnTypeDatabaseTypeName +func (r *TxResultResultSet) ColumnTypeDatabaseTypeName(index int) string { + r.checkOutput() + return r.wrap.ColumnTypeDatabaseTypeName(index) +} + +// Close implements driver.Rows/Close. +func (r *TxResultResultSet) Close() error { + r.checkOutput() + if !r.hasOutput { + return ErrInTx + } + return nil +} + +// Next implements driver.Rows/Next. +func (r *TxResultResultSet) Next(dest []driver.Value) error { + r.checkOutput() + return r.wrap.Next(dest) +} + +/*----------------------------------------------------------------------*/ + // Tx is AWS DynamoDB implementation of driver.Tx. // // @Available since v0.2.0