├── .travis.yml ├── LICENSE.txt ├── README.md ├── go.mod ├── savepoint ├── savepoint.go └── savepoint_test.go ├── txmanager.go └── txmanager_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | # go 1.1 have no option to get packages for test 4 | install: go get github.com/mattn/go-sqlite3 5 | 6 | go: 7 | - 1.7.x 8 | - 1.8.x 9 | - 1.9.x 10 | - 1.10.x 11 | - 1.11.x 12 | - tip 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ichinose Shogo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # txmanager 2 | 3 | [![Build Status](https://travis-ci.org/shogo82148/txmanager.svg?branch=master)](https://travis-ci.org/shogo82148/txmanager) 4 | 5 | The txnmanager package is a nested transation manager for database/sql. 6 | 7 | ## SYNOPSIS 8 | 9 | Use `TxBegin` to start a transaction, and `TxCommit` or `TxRollback` to finish the transaction. 10 | 11 | ``` go 12 | import ( 13 | "database/sql" 14 | 15 | "github.com/shogo82148/txmanager" 16 | ) 17 | 18 | func Example(db *sql.DB) { 19 | dbm := txmanager.NewDB(db) 20 | 21 | // start a transaction 22 | tx, _ := dbm.TxBegin() 23 | defer tx.TxFinish() 24 | 25 | // Exec INSERT in a transaction 26 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 27 | if err != nil { 28 | tx.TxRollback() 29 | } 30 | tx.TxCommit() 31 | } 32 | ``` 33 | 34 | You can manage `txmanager.DB` with `txmanager.Do`. 35 | 36 | ``` go 37 | import ( 38 | "database/sql" 39 | 40 | "github.com/shogo82148/txmanager" 41 | ) 42 | 43 | func Example(db *sql.DB) { 44 | dbm := txmanager.NewDB(db) 45 | txmanager.Do(dbm, func(tx txmanager.Tx) error { 46 | // Exec INSERT in a transaction 47 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 48 | return err 49 | }) 50 | } 51 | ``` 52 | 53 | ## NESTED TRANSACTION 54 | 55 | ``` go 56 | import ( 57 | "database/sql" 58 | 59 | "github.com/shogo82148/txmanager" 60 | ) 61 | 62 | func Foo(dbm *txmanager.DB) error { 63 | return txmanager.Do(dbm, func(tx txmanager.Tx) error { 64 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 65 | return err 66 | }) 67 | } 68 | 69 | func Example(db *sql.DB) { 70 | dbm := txmanager.NewDB(db) 71 | 72 | Foo(dbm) 73 | 74 | txmanager.Do(dbm, func(tx txmanager.Tx) error { 75 | return Foo(tx) 76 | }) 77 | } 78 | 79 | ``` 80 | 81 | ## END HOOK 82 | 83 | `TxCommit` necessarily does not do COMMIT SQL statemant. 84 | So following code sometimes outputs wrong log. 85 | 86 | ``` go 87 | func Foo(dbm *txmanager.DB) error { 88 | err := txmanager.Do(dbm, func(tx txmanager.Tx) error { 89 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)"); err != nil { 90 | return err 91 | }) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // TxCommit is success, while the transaction might fail 97 | log.Println("COMMIT is success!!!") 98 | return nil 99 | } 100 | ``` 101 | 102 | Use `TxAddEndHook` to avoid it. 103 | It is inspired by [DBIx::TransactionManager::EndHook](https://github.com/soh335/DBIx-TransactionManager-EndHook). 104 | 105 | ``` go 106 | func Foo(dbm *txmanager.DB) error { 107 | return txmanager.Do(dbm, func(tx txmanager.Tx) error { 108 | if _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)"); err != nil { 109 | return err 110 | } 111 | tx.TxAddEndHook(func() error { 112 | // It is called if all transactions are executed successfully. 113 | log.Println("COMMIT is success!!!") 114 | }) 115 | return nil 116 | }) 117 | } 118 | ``` 119 | 120 | ## LICENSE 121 | 122 | This software is released under the MIT License, see LICENSE.txt. 123 | 124 | ## godoc 125 | 126 | See [godoc](https://godoc.org/github.com/shogo82148/txmanager) for more imformation. 127 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shogo82148/txmanager 2 | -------------------------------------------------------------------------------- /savepoint/savepoint.go: -------------------------------------------------------------------------------- 1 | // The savepoint is a nested transation manager which suppurts partial rollback. 2 | package savepoint 3 | 4 | import ( 5 | "database/sql" 6 | "fmt" 7 | 8 | "github.com/shogo82148/txmanager" 9 | ) 10 | 11 | type db struct { 12 | *sql.DB 13 | } 14 | 15 | type tx struct { 16 | *sql.Tx 17 | parent *tx 18 | root *tx 19 | childCount int 20 | saveCount int 21 | done bool 22 | name string 23 | hooks []func() error 24 | } 25 | 26 | // NewDB creates new transaction manager which suppurts partial rollback. 27 | // TxBegin does SAVEPOINT, and TxCommit does RELEASE SAVEPOINT in a transaction. 28 | // TxRollback does ROLLBACK TO SAVEPOINT and finish transactions excludes parents. 29 | func NewDB(d *sql.DB) txmanager.DB { 30 | return &db{d} 31 | } 32 | 33 | func (d *db) TxBegin() (txmanager.Tx, error) { 34 | t, err := d.Begin() 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | child := &tx{Tx: t} 40 | child.root = child 41 | return child, nil 42 | } 43 | 44 | func (t *tx) TxBegin() (txmanager.Tx, error) { 45 | t.root.saveCount++ 46 | name := fmt.Sprintf("savepoint_%d", t.root.saveCount) 47 | if _, err := t.Exec("SAVEPOINT " + name); err != nil { 48 | return nil, err 49 | } 50 | 51 | t.childCount++ 52 | child := &tx{ 53 | Tx: t.Tx, 54 | parent: t, 55 | root: t.root, 56 | name: name, 57 | } 58 | return child, nil 59 | } 60 | 61 | func (t *tx) TxCommit() error { 62 | if t.done || t.root.done { 63 | return sql.ErrTxDone 64 | } 65 | 66 | if t.childCount != 0 { 67 | t.TxRollback() 68 | return txmanager.ErrChildrenNotDone 69 | } 70 | 71 | t.done = true 72 | if t.parent != nil { 73 | // t is a nested transaction. 74 | t.parent.childCount-- 75 | 76 | _, err := t.Exec("RELEASE SAVEPOINT " + t.name) 77 | if err != nil { 78 | return err 79 | } 80 | t.parent.hooks = append(t.parent.hooks, t.hooks...) 81 | return nil 82 | } 83 | 84 | // Do COMMIT 85 | err := t.Commit() 86 | if err != nil { 87 | return t.Rollback() 88 | } 89 | 90 | // call end hooks 91 | if t.hooks != nil { 92 | for _, h := range t.hooks { 93 | if err := h(); err != nil { 94 | return err 95 | } 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func (t *tx) TxRollback() error { 103 | if t.done || t.root.done { 104 | return sql.ErrTxDone 105 | } 106 | t.done = true 107 | 108 | if t.parent != nil { 109 | // t is a nested transaction. 110 | t.parent.childCount-- 111 | 112 | _, err := t.Exec("ROLLBACK TO SAVEPOINT " + t.name) 113 | return err 114 | } 115 | return t.Rollback() 116 | } 117 | 118 | func (t *tx) TxFinish() error { 119 | if t.done || t.root.done { 120 | return nil 121 | } 122 | return t.TxRollback() 123 | } 124 | 125 | func (t *tx) TxAddEndHook(hook func() error) error { 126 | if t.done || t.root.done { 127 | return sql.ErrTxDone 128 | } 129 | t.hooks = append(t.hooks, hook) 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /savepoint/savepoint_test.go: -------------------------------------------------------------------------------- 1 | package savepoint 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "testing" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | "github.com/shogo82148/txmanager" 10 | ) 11 | 12 | func setup() (*sql.DB, error) { 13 | db, err := sql.Open("sqlite3", ":memory:") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | _, err = db.Exec( 19 | "CREATE TABLE t1 (id INTEGER PRIMARY KEY)", 20 | ) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return db, nil 26 | } 27 | 28 | func TestDo(t *testing.T) { 29 | db, err := setup() 30 | if err != nil { 31 | t.Fatalf("opening database failed: %v", err) 32 | } 33 | defer db.Close() 34 | 35 | dbm := NewDB(db) 36 | err = txmanager.Do(dbm, func(tx txmanager.Tx) error { 37 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 38 | return err 39 | }) 40 | if err != nil { 41 | t.Fatalf("do failed: %v", err) 42 | } 43 | 44 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 45 | var id int 46 | if err = row.Scan(&id); err != nil { 47 | t.Fatalf("selecting row failed: %v", err) 48 | } 49 | if id != 1 { 50 | t.Errorf("got %d\nwant 1", id) 51 | } 52 | } 53 | 54 | func TestDoRollback(t *testing.T) { 55 | db, err := setup() 56 | if err != nil { 57 | t.Fatalf("opening database failed: %v", err) 58 | } 59 | defer db.Close() 60 | 61 | dbm := NewDB(db) 62 | err = txmanager.Do(dbm, func(tx txmanager.Tx) error { 63 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 64 | if err != nil { 65 | return err 66 | } 67 | return errors.New("something wrong. rollback all change.") 68 | }) 69 | if err == nil { 70 | t.Fatalf("got no error\nwant fail") 71 | } 72 | 73 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 74 | var id int 75 | if err = row.Scan(&id); err != sql.ErrNoRows { 76 | t.Errorf("got %v\nwant ErrNoRows", err) 77 | } 78 | } 79 | 80 | func TestNestCommit(t *testing.T) { 81 | db, err := setup() 82 | if err != nil { 83 | t.Fatalf("opening database failed: %v", err) 84 | } 85 | defer db.Close() 86 | 87 | dbm := NewDB(db) 88 | err = txmanager.Do(dbm, func(tx1 txmanager.Tx) error { 89 | _, err := tx1.Exec("INSERT INTO t1 (id) VALUES(1)") 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return txmanager.Do(tx1, func(tx2 txmanager.Tx) error { 95 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 96 | return err 97 | }) 98 | }) 99 | if err != nil { 100 | t.Fatalf("do failed: %v", err) 101 | } 102 | 103 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 104 | var id int 105 | if err = row.Scan(&id); err != nil { 106 | t.Fatalf("selecting row failed: %v", err) 107 | } 108 | if id != 1 { 109 | t.Errorf("got %d\nwant 1", id) 110 | } 111 | 112 | row = dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 2) 113 | if err = row.Scan(&id); err != nil { 114 | t.Fatalf("selecting row failed: %v", err) 115 | } 116 | if id != 2 { 117 | t.Errorf("got %d\nwant 2", id) 118 | } 119 | } 120 | 121 | func TestNestRollback(t *testing.T) { 122 | db, err := setup() 123 | if err != nil { 124 | t.Fatalf("opening database failed: %v", err) 125 | } 126 | defer db.Close() 127 | 128 | dbm := NewDB(db) 129 | err = txmanager.Do(dbm, func(tx1 txmanager.Tx) error { 130 | _, err := tx1.Exec("INSERT INTO t1 (id) VALUES(1)") 131 | if err != nil { 132 | t.Fatalf("intert failed: %v", err) 133 | } 134 | 135 | txmanager.Do(tx1, func(tx2 txmanager.Tx) error { 136 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 137 | if err != nil { 138 | t.Fatalf("insert failed: %v", err) 139 | } 140 | return errors.New("something wrong. rollback change.") 141 | }) 142 | 143 | _, err = tx1.Exec("INSERT INTO t1 (id) VALUES(3)") 144 | if err != nil { 145 | t.Fatalf("intert failed: %v", err) 146 | } 147 | return nil 148 | }) 149 | if err != nil { 150 | t.Fatalf("do failed: %v", err) 151 | } 152 | 153 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 154 | var id int 155 | if err = row.Scan(&id); err != nil { 156 | t.Errorf("got %v\nwant no error", err) 157 | } 158 | 159 | row = dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 2) 160 | if err = row.Scan(&id); err != sql.ErrNoRows { 161 | t.Errorf("got %v\nwant ErrNoRows", err) 162 | } 163 | 164 | row = dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 3) 165 | if err = row.Scan(&id); err != nil { 166 | t.Errorf("got %v\nwant no error", err) 167 | } 168 | } 169 | 170 | func TestTxEndHook(t *testing.T) { 171 | db, err := setup() 172 | if err != nil { 173 | t.Fatalf("opening database failed: %v", err) 174 | } 175 | defer db.Close() 176 | 177 | dbm := NewDB(db) 178 | isTx1Commited := false 179 | isTx2Commited := false 180 | err = txmanager.Do(dbm, func(tx1 txmanager.Tx) error { 181 | txmanager.Do(tx1, func(tx2 txmanager.Tx) error { 182 | tx2.TxAddEndHook(func() error { 183 | t.Error("tx2 hook is called.\nwant not to call") 184 | return nil 185 | }) 186 | return errors.New("something wrong. rollback change.") 187 | }) 188 | 189 | txmanager.Do(tx1, func(tx2 txmanager.Tx) error { 190 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 191 | tx2.TxAddEndHook(func() error { 192 | isTx2Commited = true 193 | return nil 194 | }) 195 | return err 196 | }) 197 | 198 | if isTx2Commited { 199 | t.Error("got Tx2 commited\nwant Tx2 not commited") 200 | } 201 | 202 | tx1.TxAddEndHook(func() error { 203 | if !isTx2Commited { 204 | t.Error("got Tx2 not commited\nwant Tx2 commited") 205 | } 206 | 207 | isTx1Commited = true 208 | return nil 209 | }) 210 | 211 | return nil 212 | }) 213 | 214 | if !isTx2Commited { 215 | t.Error("got Tx2 not commited\nwant Tx2 commited") 216 | } 217 | 218 | if !isTx1Commited { 219 | t.Error("got Tx1 not commited\nwant Tx1 commited") 220 | } 221 | 222 | if err != nil { 223 | t.Fatalf("do failed: %v", err) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /txmanager.go: -------------------------------------------------------------------------------- 1 | // The txnmanager is a nested transation manager for database/sql. 2 | // This software is released under the MIT License, see https://github.com/shogo82148/txmanager/blob/master/LICENSE.txt . 3 | package txmanager 4 | 5 | import ( 6 | "database/sql" 7 | "errors" 8 | ) 9 | 10 | var ErrChildrenNotDone = errors.New("txmanager: children transactions are not done") 11 | 12 | // an Executor executes SQL query. 13 | type Executor interface { 14 | // Exec executes a query without returning any rows. 15 | // See sql.DB and sql.Tx for more information. 16 | Exec(query string, args ...interface{}) (sql.Result, error) 17 | 18 | // Prepare creates a prepared statement for later queries or executions. 19 | // See sql.DB and sql.Tx for more information. 20 | Prepare(query string) (*sql.Stmt, error) 21 | 22 | // Query executes a query that returns rows, typically a SELECT. 23 | // See sql.DB and sql.Tx for more information. 24 | Query(query string, args ...interface{}) (*sql.Rows, error) 25 | 26 | // QueryRow executes a query that is expected to return at most one row. 27 | // See sql.DB and sql.Tx for more information. 28 | QueryRow(query string, args ...interface{}) *sql.Row 29 | } 30 | 31 | // a Beginner starts a transaction. 32 | type Beginner interface { 33 | // TxBegin starts a transaction. 34 | // If the DB is not a transaction, TxBegin does BEGIN at here. 35 | // Otherwise, its behavior is implementation-dependent. 36 | TxBegin() (Tx, error) 37 | } 38 | 39 | // a Committer finishes a transaction 40 | type Committer interface { 41 | // TxCommit commits the transaction. 42 | // If the DB is in a root transaction, TxCommit does COMMIT at here. 43 | // Otherwise, its behavior is implementation-dependent. 44 | TxCommit() error 45 | 46 | // TxRollback aborts the transaction. 47 | // If the DB is in a root transaction, TxRollback does ROLLBACK at here. 48 | // Otherwise, its behavior is implementation-dependent. 49 | TxRollback() error 50 | 51 | // TxFinish aborts the transaction if it is not commited. 52 | TxFinish() error 53 | } 54 | 55 | // an EndHookAdder adds end hooks 56 | type EndHookAdder interface { 57 | // TxAddEndHook add a hook function to txmanager. 58 | // Hooks are executed only all transactions are executed successfully. 59 | // If some transactions are failed, they aren't executed. 60 | TxAddEndHook(hook func() error) error 61 | } 62 | 63 | type DB interface { 64 | Executor 65 | Beginner 66 | } 67 | 68 | type Tx interface { 69 | Executor 70 | Beginner 71 | Committer 72 | EndHookAdder 73 | } 74 | 75 | type db struct { 76 | *sql.DB 77 | } 78 | 79 | type tx struct { 80 | *sql.Tx 81 | parent *tx 82 | root *tx 83 | childCount int 84 | done bool 85 | hooks []func() error 86 | } 87 | 88 | // NewDB creates a basic transaction manager. 89 | // TxBegin/TxCommit doesn't do BEGIN/COMMIT in a transaction. 90 | // They just push/pop transaction stack and do nothing. 91 | // TxRollback always does ROLLBACK and finish all related transactions includes parents. 92 | func NewDB(d *sql.DB) DB { 93 | return &db{d} 94 | } 95 | 96 | func (d *db) TxBegin() (Tx, error) { 97 | t, err := d.Begin() 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | child := &tx{Tx: t} 103 | child.root = child 104 | return child, nil 105 | } 106 | 107 | func (t *tx) TxBegin() (Tx, error) { 108 | t.childCount++ 109 | child := &tx{ 110 | Tx: t.Tx, 111 | parent: t, 112 | root: t.root, 113 | } 114 | return child, nil 115 | } 116 | 117 | func (t *tx) TxCommit() error { 118 | if t.done || t.root.done { 119 | return sql.ErrTxDone 120 | } 121 | 122 | if t.childCount != 0 { 123 | t.TxRollback() 124 | return ErrChildrenNotDone 125 | } 126 | 127 | t.done = true 128 | if t.parent != nil { 129 | // t is a nested transaction. 130 | // just pop transaction stack 131 | t.parent.childCount-- 132 | 133 | // ... and do nothing 134 | return nil 135 | } 136 | 137 | // Do COMMIT 138 | err := t.Commit() 139 | if err != nil { 140 | return t.Rollback() 141 | } 142 | 143 | // call end hooks 144 | if t.hooks != nil { 145 | for _, h := range t.hooks { 146 | if err := h(); err != nil { 147 | return err 148 | } 149 | } 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func (t *tx) TxRollback() error { 156 | if t.done || t.root.done { 157 | return sql.ErrTxDone 158 | } 159 | t.done = true 160 | t.root.done = true 161 | return t.Rollback() 162 | } 163 | 164 | func (t *tx) TxFinish() error { 165 | if t.done || t.root.done { 166 | return nil 167 | } 168 | return t.TxRollback() 169 | } 170 | 171 | func (t *tx) TxAddEndHook(hook func() error) error { 172 | if t.done || t.root.done { 173 | return sql.ErrTxDone 174 | } 175 | t.root.hooks = append(t.root.hooks, hook) 176 | return nil 177 | } 178 | 179 | // Do executes the function in a transaction. 180 | func Do(d DB, f func(t Tx) error) error { 181 | t, err := d.TxBegin() 182 | if err != nil { 183 | return err 184 | } 185 | defer t.TxFinish() 186 | err = f(t) 187 | if err != nil { 188 | return err 189 | } 190 | return t.TxCommit() 191 | } 192 | -------------------------------------------------------------------------------- /txmanager_test.go: -------------------------------------------------------------------------------- 1 | package txmanager 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "testing" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | func setup() (*sql.DB, error) { 12 | db, err := sql.Open("sqlite3", ":memory:") 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | _, err = db.Exec( 18 | "CREATE TABLE t1 (id INTEGER PRIMARY KEY)", 19 | ) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return db, nil 25 | } 26 | 27 | func TestCommit(t *testing.T) { 28 | db, err := setup() 29 | if err != nil { 30 | t.Fatalf("opening database failed: %v", err) 31 | } 32 | defer db.Close() 33 | 34 | dbm := NewDB(db) 35 | func() { 36 | tx, err := dbm.TxBegin() 37 | if err != nil { 38 | t.Fatalf("beginning transaction failed: %v", err) 39 | } 40 | defer tx.TxFinish() 41 | 42 | _, err = tx.Exec("INSERT INTO t1 (id) VALUES(1)") 43 | if err != nil { 44 | t.Fatalf("inserting failed: %v", err) 45 | } 46 | 47 | if err := tx.TxCommit(); err != nil { 48 | t.Fatalf("commiting failed: %v", err) 49 | } 50 | }() 51 | 52 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 53 | var id int 54 | if err = row.Scan(&id); err != nil { 55 | t.Fatalf("selecting row failed: %v", err) 56 | } 57 | if id != 1 { 58 | t.Errorf("got %d\nwant 1", id) 59 | } 60 | } 61 | 62 | func TestRollback(t *testing.T) { 63 | db, err := setup() 64 | if err != nil { 65 | t.Fatalf("opening database failed: %v", err) 66 | } 67 | defer db.Close() 68 | 69 | dbm := NewDB(db) 70 | func() { 71 | tx, err := dbm.TxBegin() 72 | if err != nil { 73 | t.Fatalf("beginning transaction failed: %v", err) 74 | } 75 | defer tx.TxFinish() 76 | 77 | _, err = tx.Exec("INSERT INTO t1 (id) VALUES(1)") 78 | if err != nil { 79 | t.Fatalf("inserting failed: %v", err) 80 | } 81 | 82 | if err := tx.TxRollback(); err != nil { 83 | t.Fatalf("rollback failed: %v", err) 84 | } 85 | }() 86 | 87 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 88 | var id int 89 | if err = row.Scan(&id); err != sql.ErrNoRows { 90 | t.Fatalf("got %v\nwant ErrNoRows", err) 91 | } 92 | } 93 | 94 | func TestDo(t *testing.T) { 95 | db, err := setup() 96 | if err != nil { 97 | t.Fatalf("opening database failed: %v", err) 98 | } 99 | defer db.Close() 100 | 101 | dbm := NewDB(db) 102 | err = Do(dbm, func(tx Tx) error { 103 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 104 | return err 105 | }) 106 | if err != nil { 107 | t.Fatalf("do failed: %v", err) 108 | } 109 | 110 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 111 | var id int 112 | if err = row.Scan(&id); err != nil { 113 | t.Fatalf("selecting row failed: %v", err) 114 | } 115 | if id != 1 { 116 | t.Errorf("got %d\nwant 1", id) 117 | } 118 | } 119 | 120 | func TestDoPanic(t *testing.T) { 121 | db, err := setup() 122 | if err != nil { 123 | t.Fatalf("opening database failed: %v", err) 124 | } 125 | defer db.Close() 126 | 127 | dbm := NewDB(db) 128 | myerror := "something wrong!!!!" 129 | func() { 130 | defer func() { 131 | if err := recover(); err != nil && err != myerror { 132 | t.Error("unexpected panic:", err) 133 | } 134 | }() 135 | Do(dbm, func(tx Tx) error { 136 | _, err := tx.Exec("INSERT INTO t1 (id) VALUES(1)") 137 | if err != nil { 138 | return err 139 | } 140 | panic(myerror) 141 | return nil 142 | }) 143 | }() 144 | 145 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 146 | var id int 147 | if err = row.Scan(&id); err != sql.ErrNoRows { 148 | t.Errorf("got %v\nwant ErrNoRows", err) 149 | } 150 | } 151 | 152 | func TestNestCommit(t *testing.T) { 153 | db, err := setup() 154 | if err != nil { 155 | t.Fatalf("opening database failed: %v", err) 156 | } 157 | defer db.Close() 158 | 159 | dbm := NewDB(db) 160 | err = Do(dbm, func(tx1 Tx) error { 161 | _, err := tx1.Exec("INSERT INTO t1 (id) VALUES(1)") 162 | if err != nil { 163 | return err 164 | } 165 | 166 | return Do(tx1, func(tx2 Tx) error { 167 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 168 | return err 169 | }) 170 | }) 171 | if err != nil { 172 | t.Fatalf("do failed: %v", err) 173 | } 174 | 175 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 176 | var id int 177 | if err = row.Scan(&id); err != nil { 178 | t.Fatalf("selecting row failed: %v", err) 179 | } 180 | if id != 1 { 181 | t.Errorf("got %d\nwant 1", id) 182 | } 183 | 184 | row = dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 2) 185 | if err = row.Scan(&id); err != nil { 186 | t.Fatalf("selecting row failed: %v", err) 187 | } 188 | if id != 2 { 189 | t.Errorf("got %d\nwant 2", id) 190 | } 191 | } 192 | 193 | func TestNestRollback(t *testing.T) { 194 | db, err := setup() 195 | if err != nil { 196 | t.Fatalf("opening database failed: %v", err) 197 | } 198 | defer db.Close() 199 | 200 | dbm := NewDB(db) 201 | err = Do(dbm, func(tx1 Tx) error { 202 | _, err := tx1.Exec("INSERT INTO t1 (id) VALUES(1)") 203 | if err != nil { 204 | t.Fatalf("intert failed: %v", err) 205 | } 206 | 207 | err = Do(tx1, func(tx2 Tx) error { 208 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 209 | return err 210 | }) 211 | if err != nil { 212 | t.Fatalf("insert failed: %v", err) 213 | } 214 | 215 | return errors.New("something wrong. rollback all change.") 216 | }) 217 | if err == nil { 218 | t.Fatalf("got no error\nwant fail") 219 | } 220 | 221 | row := dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 1) 222 | var id int 223 | if err = row.Scan(&id); err != sql.ErrNoRows { 224 | t.Errorf("got %v\nwant ErrNoRows", err) 225 | } 226 | 227 | row = dbm.QueryRow("SELECT id FROM t1 WHERE id = ?", 2) 228 | if err = row.Scan(&id); err != sql.ErrNoRows { 229 | t.Errorf("got %v\nwant ErrNoRows", err) 230 | } 231 | } 232 | 233 | func TestTxEndHook(t *testing.T) { 234 | db, err := setup() 235 | if err != nil { 236 | t.Fatalf("opening database failed: %v", err) 237 | } 238 | defer db.Close() 239 | 240 | dbm := NewDB(db) 241 | isTx1Commited := false 242 | isTx2Commited := false 243 | err = Do(dbm, func(tx1 Tx) error { 244 | Do(tx1, func(tx2 Tx) error { 245 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 246 | tx2.TxAddEndHook(func() error { 247 | isTx2Commited = true 248 | return nil 249 | }) 250 | return err 251 | }) 252 | 253 | if isTx2Commited { 254 | t.Error("got Tx2 commited\nwant Tx2 not commited") 255 | } 256 | 257 | tx1.TxAddEndHook(func() error { 258 | if !isTx2Commited { 259 | t.Error("got Tx2 not commited\nwant Tx2 commited") 260 | } 261 | 262 | isTx1Commited = true 263 | return nil 264 | }) 265 | 266 | return nil 267 | }) 268 | 269 | if !isTx2Commited { 270 | t.Error("got Tx2 not commited\nwant Tx2 commited") 271 | } 272 | 273 | if !isTx1Commited { 274 | t.Error("got Tx1 not commited\nwant Tx1 commited") 275 | } 276 | 277 | if err != nil { 278 | t.Fatalf("do failed: %v", err) 279 | } 280 | } 281 | 282 | func TestTxEndHookRollback(t *testing.T) { 283 | db, err := setup() 284 | if err != nil { 285 | t.Fatalf("opening database failed: %v", err) 286 | } 287 | defer db.Close() 288 | 289 | dbm := NewDB(db) 290 | err = Do(dbm, func(tx1 Tx) error { 291 | Do(tx1, func(tx2 Tx) error { 292 | _, err := tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 293 | tx2.TxAddEndHook(func() error { 294 | t.Error("tx2 hook is called.\nwant not to call") 295 | return nil 296 | }) 297 | return err 298 | }) 299 | 300 | tx1.TxAddEndHook(func() error { 301 | t.Error("tx1 hook is called.\nwant not to call") 302 | return nil 303 | }) 304 | 305 | return errors.New("something wrong. rollback all change.") 306 | }) 307 | } 308 | 309 | func TestTxEndHookError(t *testing.T) { 310 | db, err := setup() 311 | if err != nil { 312 | t.Fatalf("opening database failed: %v", err) 313 | } 314 | defer db.Close() 315 | 316 | dbm := NewDB(db) 317 | err = Do(dbm, func(tx1 Tx) error { 318 | Do(tx1, func(tx2 Tx) error { 319 | tx2.Exec("INSERT INTO t1 (id) VALUES(2)") 320 | tx2.TxAddEndHook(func() error { 321 | return errors.New("something wrong. rollback all change.") 322 | }) 323 | return err 324 | }) 325 | 326 | tx1.TxAddEndHook(func() error { 327 | t.Error("tx1 hook is called.\nwant not to call") 328 | return nil 329 | }) 330 | 331 | return nil 332 | }) 333 | } 334 | --------------------------------------------------------------------------------