├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── LICENSE ├── Makefile ├── README.md ├── context.go ├── db.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── options.go └── tests ├── bootstrap_test.go ├── db_test.go ├── go.mod └── go.sum /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [l3pp4rd, flimzy, Yiling-J] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | detect-modules: 6 | runs-on: ubuntu-latest 7 | outputs: 8 | modules: ${{ steps.set-modules.outputs.modules }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-go@v5 12 | with: 13 | go-version: ${{ env.GO_VERSION }} 14 | - id: set-modules 15 | run: echo "modules=$(go list -m -json | jq -s '.' | jq -c '[.[].Dir]')" >> $GITHUB_OUTPUT 16 | 17 | golangci-lint: 18 | needs: detect-modules 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ env.GO_VERSION }} 28 | - name: golangci-lint ${{ matrix.modules }} 29 | uses: golangci/golangci-lint-action@v6 30 | with: 31 | version: ${{ env.GOLANGCI_LINT_VERSION }} 32 | working-directory: ${{ matrix.modules }} 33 | 34 | integration: 35 | runs-on: ubuntu-latest 36 | strategy: 37 | matrix: 38 | go: ["1.21.x", "1.22.x", "1.23.x", "1.24.x"] 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: actions/setup-go@v4 42 | with: 43 | go-version: ${{ matrix.go }} 44 | - name: Run integration tests 45 | working-directory: ./tests 46 | env: 47 | MYSQL_DSN: AUTO 48 | PSQL_DSN: AUTO 49 | run: go test ./... 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses) 2 | 3 | Copyright (c) 2015-2020, gediminas.morkevicius@gmail.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * The name DataDog.lt may not be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, 23 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 26 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: MYSQL_DSN=root:pass@/txdb_test 2 | test: PSQL_DSN=postgres://postgres:pass@localhost/txdb_test 3 | test: mysql psql 4 | @cd tests && go test -race 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/DATA-DOG/go-txdb.svg?branch=master)](https://travis-ci.org/DATA-DOG/go-txdb) 2 | [![GoDoc](https://godoc.org/github.com/DATA-DOG/go-txdb?status.svg)](https://godoc.org/github.com/DATA-DOG/go-txdb) 3 | 4 | # Single transaction based sql.Driver for GO 5 | 6 | Package **txdb** is a single transaction based database sql driver. When the connection 7 | is opened, it starts a transaction and all operations performed on this **sql.DB** 8 | will be within that transaction. If concurrent actions are performed, the lock is 9 | acquired and connection is always released the statements and rows are not holding the 10 | connection. 11 | 12 | Why is it useful. A very basic use case would be if you want to make functional tests 13 | you can prepare a test database and within each test you do not have to reload a database. 14 | All tests are isolated within transaction and though, performs fast. And you do not have 15 | to interface your **sql.DB** reference in your code, **txdb** is like a standard **sql.Driver**. 16 | 17 | This driver supports any **sql.Driver** connection to be opened. You can register txdb 18 | for different sql drivers and have it under different driver names. Under the hood 19 | whenever a txdb driver is opened, it attempts to open a real connection and starts 20 | transaction. When close is called, it rollbacks transaction leaving your prepared 21 | test database in the same state as before. 22 | 23 | Given, you have a mysql database called **txdb_test** and a table **users** with a **username** 24 | column. 25 | 26 | ``` go 27 | package main 28 | 29 | import ( 30 | "database/sql" 31 | "log" 32 | 33 | "github.com/DATA-DOG/go-txdb" 34 | _ "github.com/go-sql-driver/mysql" 35 | ) 36 | 37 | func init() { 38 | // we register an sql driver named "txdb" 39 | txdb.Register("txdb", "mysql", "root@/txdb_test") 40 | } 41 | 42 | func main() { 43 | // dsn serves as an unique identifier for connection pool 44 | db, err := sql.Open("txdb", "identifier") 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | defer db.Close() 49 | 50 | if _, err := db.Exec(`INSERT INTO users(username) VALUES("gopher")`); err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | ``` 55 | 56 | You can also use [`sql.OpenDB`](https://golang.org/pkg/database/sql/#OpenDB) (added in Go 1.10) rather than registering a txdb driver instance, if you prefer: 57 | 58 | ``` go 59 | package main 60 | 61 | import ( 62 | "database/sql" 63 | "log" 64 | 65 | "github.com/DATA-DOG/go-txdb" 66 | _ "github.com/go-sql-driver/mysql" 67 | ) 68 | 69 | func main() { 70 | db := sql.OpenDB(txdb.New("mysql", "root@/txdb_test")) 71 | defer db.Close() 72 | 73 | if _, err := db.Exec(`INSERT INTO users(username) VALUES("gopher")`); err != nil { 74 | log.Fatal(err) 75 | } 76 | } 77 | ``` 78 | 79 | Every time you will run this application, it will remain in the same state as before. 80 | 81 | ### Testing 82 | 83 | Usage is mainly intended for testing purposes. Tests require database access, support using `postgres` and `mysql` databases. The easiest way to do this is by using [testcontainers](https://golang.testcontainers.org/), which is enabled by setting the respective database DSN values to `AUTO`. Example: 84 | 85 | ```bash 86 | MYSQL_DSN=AUTO PSQL_DSN=AUTO go test ./... 87 | ``` 88 | 89 | If you wish to use a running local database instance, you can also provide the DSN directly, and it will be used: 90 | 91 | ```bash 92 | MYSQL_DSN=root:pass@/ PSQL_DSN=postgres://postgres:pass@localhost/ go test ./... 93 | ``` 94 | 95 | To run tests only against MySQL or PostgreSQL, you may provide only the respective DSN values; any unset DSN is skipped for tests. 96 | 97 | ### Documentation 98 | 99 | See [godoc][godoc] for general API details. 100 | See **.travis.yml** for supported **go** versions. 101 | 102 | ### Contributions 103 | 104 | Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) - 105 | please open an issue before to discuss whether these changes can be accepted. All backward incompatible changes are 106 | and will be treated cautiously. 107 | 108 | The public API is locked since it is an **sql.Driver** and will not change. 109 | 110 | ### License 111 | 112 | **txdb** is licensed under the [three clause BSD license][license] 113 | 114 | [godoc]: http://godoc.org/github.com/DATA-DOG/go-txdb "Documentation on 115 | godoc" 116 | 117 | [golang]: https://golang.org/ "GO programming language" 118 | 119 | [license]:http://en.wikipedia.org/wiki/BSD_licenses "The three clause BSD license" 120 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package txdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "database/sql/driver" 7 | "io" 8 | ) 9 | 10 | func buildRows(r *sql.Rows) (driver.Rows, error) { 11 | set := &rowSets{} 12 | rs := &rows{} 13 | if err := rs.read(r); err != nil { 14 | return set, err 15 | } 16 | set.sets = append(set.sets, rs) 17 | for r.NextResultSet() { 18 | rss := &rows{} 19 | if err := rss.read(r); err != nil { 20 | return set, err 21 | } 22 | set.sets = append(set.sets, rss) 23 | } 24 | return set, nil 25 | } 26 | 27 | // Implement the "RowsNextResultSet" interface 28 | func (rs *rowSets) HasNextResultSet() bool { 29 | return rs.pos+1 < len(rs.sets) 30 | } 31 | 32 | // Implement the "RowsNextResultSet" interface 33 | func (rs *rowSets) NextResultSet() error { 34 | if !rs.HasNextResultSet() { 35 | return io.EOF 36 | } 37 | 38 | rs.pos++ 39 | return nil 40 | } 41 | 42 | func (c *conn) beginTxOnce(ctx context.Context, done <-chan struct{}) (*sql.Tx, error) { 43 | if c.tx == nil { 44 | rootCtx, cancel := context.WithCancel(context.Background()) 45 | tx, err := c.drv.db.BeginTx(rootCtx, &sql.TxOptions{}) 46 | if err != nil { 47 | cancel() 48 | return nil, err 49 | } 50 | c.tx, c.ctx, c.cancel = tx, rootCtx, cancel 51 | } 52 | go func() { 53 | select { 54 | case <-ctx.Done(): 55 | select { 56 | case <-done: 57 | // the operation successfully finished at the "same time" as context cancellation, so we won't close ctx on tx 58 | default: 59 | // operation was interrupted by context cancel, so we cancel parent as well 60 | c.cancel() 61 | } 62 | case <-done: 63 | // operation was successfully finished, so we don't close ctx on tx 64 | case <-c.ctx.Done(): 65 | } 66 | }() 67 | return c.tx, nil 68 | } 69 | 70 | // Implement the "QueryerContext" interface 71 | func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { 72 | c.Lock() 73 | defer c.Unlock() 74 | 75 | done := make(chan struct{}) 76 | defer close(done) 77 | 78 | tx, err := c.beginTxOnce(ctx, done) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | rs, err := tx.QueryContext(ctx, query, mapNamedArgs(args)...) 84 | if err != nil { 85 | return nil, err 86 | } 87 | defer rs.Close() 88 | 89 | return buildRows(rs) 90 | } 91 | 92 | // Implement the "ExecerContext" interface 93 | func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { 94 | c.Lock() 95 | defer c.Unlock() 96 | 97 | done := make(chan struct{}) 98 | defer close(done) 99 | 100 | tx, err := c.beginTxOnce(ctx, done) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return tx.ExecContext(ctx, query, mapNamedArgs(args)...) 106 | } 107 | 108 | // Implement the "ConnBeginTx" interface 109 | func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { 110 | return c.Begin() 111 | } 112 | 113 | // Implement the "ConnPrepareContext" interface 114 | func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { 115 | c.Lock() 116 | defer c.Unlock() 117 | 118 | done := make(chan struct{}) 119 | defer close(done) 120 | 121 | tx, err := c.beginTxOnce(ctx, done) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | st, err := tx.PrepareContext(ctx, query) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | stmtFailedStr := make(chan bool) 132 | go func() { 133 | select { 134 | case <-c.ctx.Done(): 135 | case erred := <-stmtFailedStr: 136 | if erred { 137 | st.Close() 138 | } 139 | } 140 | }() 141 | return &stmt{st: st, done: stmtFailedStr}, nil 142 | } 143 | 144 | // Implement the "Pinger" interface 145 | func (c *conn) Ping(ctx context.Context) error { 146 | return c.drv.db.PingContext(ctx) 147 | } 148 | 149 | // Implement the "StmtExecContext" interface 150 | func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { 151 | dr, err := s.st.ExecContext(ctx, mapNamedArgs(args)...) 152 | if err != nil { 153 | s.closeDone(true) 154 | } 155 | return dr, err 156 | } 157 | 158 | // Implement the "StmtQueryContext" interface 159 | func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { 160 | rows, err := s.st.QueryContext(ctx, mapNamedArgs(args)...) 161 | if err != nil { 162 | s.closeDone(true) 163 | return nil, err 164 | } 165 | return buildRows(rows) 166 | } 167 | 168 | func mapNamedArgs(args []driver.NamedValue) (res []interface{}) { 169 | res = make([]interface{}, len(args)) 170 | for i := range args { 171 | name := args[i].Name 172 | if name != "" { 173 | res[i] = sql.Named(name, args[i].Value) 174 | } else { 175 | res[i] = args[i].Value 176 | } 177 | } 178 | return 179 | } 180 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package txdb is a single transaction based [database/sql/driver] implementation. 3 | When the connection is opened, it starts a transaction and all operations 4 | performed on the returned [database/sql.DB] will be within that transaction. If 5 | concurrent actions are performed, the lock is acquired and connection is always 6 | released the statements and rows are not holding the connection. 7 | 8 | Why is it useful? A very basic use case would be if you want to make functional 9 | tests, you can prepare a test database and within each test you do not have to 10 | reload a database. All tests are isolated within a transaction and execute fast. 11 | And you do not have to interface your [database/sql.DB] reference in your code, 12 | txdb is like a standard [database/sql/driver.Driver]. 13 | 14 | This driver supports any [database/sql/driver.Driver] connection to be opened. 15 | You can register txdb for different drivers and have it under different driver 16 | names. Under the hood whenever a txdb driver is opened, it attempts to open a 17 | real connection and starts transaction. When close is called, it rollbacks 18 | transaction leaving your prepared test database in the same state as before. 19 | 20 | Example, assuming you have a mysql database called txdb_test and a table users with a 21 | username: 22 | 23 | package main 24 | 25 | import ( 26 | "database/sql" 27 | "log" 28 | 29 | "github.com/DATA-DOG/go-txdb" 30 | _ "github.com/go-sql-driver/mysql" 31 | ) 32 | 33 | func init() { 34 | // we register an sql driver named "txdb" 35 | txdb.Register("txdb", "mysql", "root@/txdb_test") 36 | } 37 | 38 | func main() { 39 | // dsn serves as an unique identifier for connection pool 40 | db, err := sql.Open("txdb", "identifier") 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | defer db.Close() 45 | 46 | if _, err := db.Exec(`INSERT INTO users(username) VALUES("gopher")`); err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | 51 | Every time you will run this application, it will remain in the same state as before. 52 | */ 53 | package txdb 54 | 55 | import ( 56 | "context" 57 | "database/sql" 58 | "database/sql/driver" 59 | "fmt" 60 | "io" 61 | "reflect" 62 | "sync" 63 | ) 64 | 65 | // New returns a [database/sql/driver.Connector], which can be passed to 66 | // [database/sql.OpenDB]. This can be used in place of [Register]. 67 | // It takes the same arguments as [Register], with the omission of name. 68 | func New(drv, dsn string, options ...func(*conn) error) driver.Connector { 69 | return &txConnector{ 70 | driver: &TxDriver{ 71 | dsn: dsn, 72 | drv: drv, 73 | conns: make(map[string]*conn), 74 | options: options, 75 | }, 76 | name: "connector", 77 | } 78 | } 79 | 80 | // Register registers a txdb sql driver under the given sql driver name 81 | // which can be used to open a single transaction based database connection. 82 | // 83 | // When Open is called any number of times it returns the same transaction 84 | // connection. 85 | // 86 | // Any Begin, Commit calls will not start or close the transaction. 87 | // Instead the savepoint will be created, released, or rolled back. 88 | // If your SQL driver does not support save points, use nil 89 | // for the SavePointOption argument. If driver has non-default 90 | // save point logic, you can override the default with SavePointOption. 91 | // 92 | // When [Close] is called, the transaction is rolled back. 93 | // 94 | // The drv dsn are passed to [databse/sql.Open]. 95 | // 96 | // Note: if you open a secondary database, make sure to differentiate 97 | // the dsn string when opening the [driver/sql.DB]. The transaction will be 98 | // isolated within that dsn. 99 | func Register(name, drv, dsn string, options ...func(*conn) error) { 100 | sql.Register(name, &TxDriver{ 101 | dsn: dsn, 102 | drv: drv, 103 | conns: make(map[string]*conn), 104 | options: options, 105 | }) 106 | } 107 | 108 | type conn struct { 109 | sync.Mutex 110 | tx *sql.Tx 111 | dsn string 112 | opened uint 113 | drv *TxDriver 114 | saves uint 115 | savePoint SavePoint 116 | 117 | cancel func() 118 | ctx interface{ Done() <-chan struct{} } 119 | } 120 | 121 | // TxDriver is a [database/sql/driver.Driver] implementation which runs on 122 | // single transaction. When [database/sql.DB.Close] is called, transaction is 123 | // rolled back. 124 | type TxDriver struct { 125 | sync.Mutex 126 | db *sql.DB 127 | realConn driver.Conn // Meant to be used as NamedValueChecker 128 | conns map[string]*conn 129 | options []func(*conn) error 130 | 131 | drv string 132 | dsn string 133 | } 134 | 135 | var ( 136 | _ driver.Driver = (*TxDriver)(nil) 137 | _ driver.DriverContext = (*TxDriver)(nil) 138 | ) 139 | 140 | type txConnector struct { 141 | driver *TxDriver 142 | name string 143 | } 144 | 145 | var _ driver.Connector = (*txConnector)(nil) 146 | 147 | // Connect satisfies the [database/sql/driver.Connector] interface. 148 | func (c *txConnector) Connect(context.Context) (driver.Conn, error) { 149 | // The DSN passed here doesn't matter, since it's only used to disambiguate 150 | // connections, but that disambiguation happens in the call to New() when 151 | // used through the driver.Connector interface. 152 | return c.driver.Open(c.name) 153 | } 154 | 155 | // Driver satisfies the [database/sql/driver.Connector] interface. 156 | func (c *txConnector) Driver() driver.Driver { 157 | return c.driver 158 | } 159 | 160 | func (d *TxDriver) DB() *sql.DB { 161 | return d.db 162 | } 163 | 164 | // OpenConnector satisfies the [database/sql/driver.DriverContext] interface. 165 | func (d *TxDriver) OpenConnector(name string) (driver.Connector, error) { 166 | return &txConnector{ 167 | driver: d, 168 | name: name, 169 | }, nil 170 | } 171 | 172 | func (d *TxDriver) Open(dsn string) (driver.Conn, error) { 173 | d.Lock() 174 | defer d.Unlock() 175 | // first open a real database connection 176 | if d.db == nil { 177 | db, err := sql.Open(d.drv, d.dsn) 178 | if err != nil { 179 | return nil, err 180 | } 181 | d.db = db 182 | 183 | realConn, err := db.Driver().Open(d.dsn) 184 | if err != nil { 185 | return nil, err 186 | } 187 | d.realConn = realConn 188 | } 189 | c, ok := d.conns[dsn] 190 | if !ok { 191 | c = &conn{ 192 | dsn: dsn, 193 | drv: d, 194 | savePoint: &defaultSavePoint{}, 195 | cancel: func() {}, 196 | ctx: stubCtx{}, 197 | } 198 | for _, opt := range d.options { 199 | if e := opt(c); e != nil { 200 | return c, e 201 | } 202 | } 203 | d.conns[dsn] = c 204 | } 205 | c.opened++ // safe since conn.Close() must acquire driver lock first 206 | return c, nil 207 | } 208 | 209 | func (d *TxDriver) deleteConn(dsn string) error { 210 | // d must be locked before call 211 | delete(d.conns, dsn) 212 | if len(d.conns) == 0 && d.db != nil { 213 | if err := d.db.Close(); err != nil { 214 | return err 215 | } 216 | d.db = nil 217 | if err := d.realConn.Close(); err != nil { 218 | return err 219 | } 220 | } 221 | return nil 222 | } 223 | 224 | func (c *conn) beginOnce() (*sql.Tx, error) { 225 | if c.tx == nil { 226 | tx, err := c.drv.db.Begin() 227 | if err != nil { 228 | return nil, err 229 | } 230 | c.tx = tx 231 | } 232 | return c.tx, nil 233 | } 234 | 235 | func (c *conn) Close() (err error) { 236 | c.drv.Lock() 237 | defer c.drv.Unlock() 238 | 239 | c.opened-- 240 | if c.opened == 0 { 241 | if c.tx != nil { 242 | err := c.tx.Rollback() 243 | if err != nil { 244 | return err 245 | } 246 | c.cancel() 247 | c.tx = nil 248 | } 249 | return c.drv.deleteConn(c.dsn) 250 | } 251 | return 252 | } 253 | 254 | type tx struct { 255 | id string 256 | conn *conn 257 | } 258 | 259 | func (c *conn) Begin() (driver.Tx, error) { 260 | if c.savePoint == nil { 261 | return &tx{"_", c}, nil // save point is not supported 262 | } 263 | 264 | c.Lock() 265 | defer c.Unlock() 266 | 267 | connTx, err := c.beginOnce() 268 | if err != nil { 269 | return nil, err 270 | } 271 | 272 | c.saves++ 273 | id := fmt.Sprintf("tx_%d", c.saves) 274 | _, err = connTx.Exec(c.savePoint.Create(id)) 275 | if err != nil { 276 | return nil, err 277 | } 278 | return &tx{id, c}, nil 279 | } 280 | 281 | func (tx *tx) Commit() error { 282 | if tx.conn.savePoint == nil { 283 | return nil // save point is not supported 284 | } 285 | 286 | tx.conn.Lock() 287 | defer tx.conn.Unlock() 288 | 289 | connTx, err := tx.conn.beginOnce() 290 | if err != nil { 291 | return err 292 | } 293 | 294 | _, err = connTx.Exec(tx.conn.savePoint.Release(tx.id)) 295 | return err 296 | } 297 | 298 | func (tx *tx) Rollback() error { 299 | if tx.conn.savePoint == nil { 300 | return nil // save point is not supported 301 | } 302 | 303 | tx.conn.Lock() 304 | defer tx.conn.Unlock() 305 | 306 | connTx, err := tx.conn.beginOnce() 307 | if err != nil { 308 | return err 309 | } 310 | 311 | _, err = connTx.Exec(tx.conn.savePoint.Rollback(tx.id)) 312 | return err 313 | } 314 | 315 | func (c *conn) Prepare(query string) (driver.Stmt, error) { 316 | c.Lock() 317 | defer c.Unlock() 318 | 319 | tx, err := c.beginOnce() 320 | if err != nil { 321 | return nil, err 322 | } 323 | 324 | st, err := tx.Prepare(query) 325 | if err != nil { 326 | return nil, err 327 | } 328 | return &stmt{st: st}, nil 329 | } 330 | 331 | func (c *conn) Exec(query string, args []driver.Value) (driver.Result, error) { 332 | c.Lock() 333 | defer c.Unlock() 334 | 335 | tx, err := c.beginOnce() 336 | if err != nil { 337 | return nil, err 338 | } 339 | 340 | return tx.Exec(query, mapArgs(args)...) 341 | } 342 | 343 | func mapArgs(args []driver.Value) (res []interface{}) { 344 | res = make([]interface{}, len(args)) 345 | for i := range args { 346 | res[i] = args[i] 347 | } 348 | return 349 | } 350 | 351 | func (c *conn) Query(query string, args []driver.Value) (driver.Rows, error) { 352 | c.Lock() 353 | defer c.Unlock() 354 | 355 | tx, err := c.beginOnce() 356 | if err != nil { 357 | return nil, err 358 | } 359 | 360 | // query rows 361 | rs, err := tx.Query(query, mapArgs(args)...) 362 | if err != nil { 363 | return nil, err 364 | } 365 | defer rs.Close() 366 | 367 | return buildRows(rs) 368 | } 369 | 370 | // Implement the NamedValueChecker interface 371 | func (c *conn) CheckNamedValue(nv *driver.NamedValue) error { 372 | if nvc, ok := c.drv.realConn.(driver.NamedValueChecker); ok { 373 | return nvc.CheckNamedValue(nv) 374 | } 375 | 376 | switch nv.Value.(type) { 377 | case sql.Out: 378 | return nil 379 | default: 380 | return driver.ErrSkip 381 | } 382 | } 383 | 384 | type stmt struct { 385 | mu sync.Mutex 386 | st *sql.Stmt 387 | done chan bool 388 | } 389 | 390 | func (s *stmt) Exec(args []driver.Value) (driver.Result, error) { 391 | dr, err := s.st.Exec(mapArgs(args)...) 392 | if err != nil { 393 | s.closeDone(true) 394 | } 395 | return dr, err 396 | } 397 | 398 | func (s *stmt) NumInput() int { 399 | return -1 400 | } 401 | 402 | func (s *stmt) Close() error { 403 | s.closeDone(false) 404 | return s.st.Close() 405 | } 406 | 407 | func (s *stmt) Query(args []driver.Value) (driver.Rows, error) { 408 | rows, err := s.st.Query(mapArgs(args)...) 409 | if err != nil { 410 | s.closeDone(true) 411 | return nil, err 412 | } 413 | return buildRows(rows) 414 | } 415 | 416 | func (s *stmt) closeDone(withErr bool) { 417 | s.mu.Lock() 418 | defer s.mu.Unlock() 419 | if s.done == nil { 420 | return 421 | } 422 | 423 | select { 424 | case s.done <- withErr: 425 | default: 426 | } 427 | 428 | close(s.done) 429 | s.done = nil 430 | } 431 | 432 | type rows struct { 433 | rows [][]driver.Value 434 | pos int 435 | cols []string 436 | colTypes []*sql.ColumnType 437 | } 438 | 439 | func (r *rows) Columns() []string { 440 | return r.cols 441 | } 442 | 443 | func (r *rows) ColumnTypeDatabaseTypeName(index int) string { 444 | return r.colTypes[index].DatabaseTypeName() 445 | } 446 | 447 | func (r *rows) Next(dest []driver.Value) error { 448 | r.pos++ 449 | if r.pos > len(r.rows) { 450 | return io.EOF 451 | } 452 | 453 | for i, val := range r.rows[r.pos-1] { 454 | dest[i] = *(val.(*interface{})) 455 | } 456 | 457 | return nil 458 | } 459 | 460 | func (r *rows) Close() error { 461 | return nil 462 | } 463 | 464 | func (r *rows) read(rs *sql.Rows) error { 465 | var err error 466 | r.cols, err = rs.Columns() 467 | if err != nil { 468 | return err 469 | } 470 | 471 | r.colTypes, err = rs.ColumnTypes() 472 | if err != nil { 473 | return err 474 | } 475 | 476 | for rs.Next() { 477 | values := make([]interface{}, len(r.cols)) 478 | for i := range values { 479 | values[i] = new(interface{}) 480 | } 481 | if err := rs.Scan(values...); err != nil { 482 | return err 483 | } 484 | row := make([]driver.Value, len(r.cols)) 485 | for i, v := range values { 486 | row[i] = driver.Value(v) 487 | } 488 | r.rows = append(r.rows, row) 489 | } 490 | return rs.Err() 491 | } 492 | 493 | type rowSets struct { 494 | sets []*rows 495 | pos int 496 | } 497 | 498 | func (rs *rowSets) Columns() []string { 499 | return rs.sets[rs.pos].cols 500 | } 501 | 502 | func (rs *rowSets) ColumnTypeDatabaseTypeName(index int) string { 503 | return rs.sets[rs.pos].ColumnTypeDatabaseTypeName(index) 504 | } 505 | 506 | func (rs *rowSets) ColumnTypeScanType(index int) reflect.Type { 507 | return rs.sets[rs.pos].colTypes[index].ScanType() 508 | } 509 | 510 | func (rs *rowSets) Close() error { 511 | return nil 512 | } 513 | 514 | // advances to next row 515 | func (rs *rowSets) Next(dest []driver.Value) error { 516 | return rs.sets[rs.pos].Next(dest) 517 | } 518 | 519 | type stubCtx struct{} 520 | 521 | func (s stubCtx) Done() <-chan struct{} { 522 | return nil 523 | } 524 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DATA-DOG/go-txdb 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DATA-DOG/go-txdb/3cc9573e9aac1b23dd33c2e1a93addfb957bbd7a/go.sum -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.0 2 | 3 | use ( 4 | . 5 | ./tests 6 | ) 7 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= 2 | github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= 3 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 4 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 5 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= 7 | github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= 8 | github.com/containerd/btrfs/v2 v2.0.0/go.mod h1:swkD/7j9HApWpzl8OHfrHNxppPd9l44DFZdF94BUj9k= 9 | github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= 10 | github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= 11 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 12 | github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 13 | github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= 14 | github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= 15 | github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM= 16 | github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= 17 | github.com/containerd/imgcrypt v1.1.8/go.mod h1:x6QvFIkMyO2qGIY2zXc88ivEzcbgvLdWjoZyGqDap5U= 18 | github.com/containerd/nri v0.6.1/go.mod h1:7+sX3wNx+LR7RzhjnJiUkFDhn18P5Bg/0VnJ/uXpRJM= 19 | github.com/containerd/ttrpc v1.2.4/go.mod h1:ojvb8SJBSch0XkqNO0L0YX/5NxR3UnVk2LzFKBK0upc= 20 | github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= 21 | github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= 22 | github.com/containerd/zfs v1.1.0/go.mod h1:oZF9wBnrnQjpWLaPKEinrx3TQ9a+W/RJO7Zb41d8YLE= 23 | github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw= 24 | github.com/containernetworking/plugins v1.2.0/go.mod h1:/VjX4uHecW5vVimFa1wkG4s+r/s9qIfPdqlLF4TW8c4= 25 | github.com/containers/ocicrypt v1.1.10/go.mod h1:YfzSSr06PTHQwSTUKqDSjish9BeW1E4HUmreluQcMd8= 26 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 27 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 28 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 29 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 30 | github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 31 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 32 | github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 33 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 34 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 35 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 36 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 37 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= 38 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 39 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 40 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 41 | github.com/intel/goresctrl v0.3.0/go.mod h1:fdz3mD85cmP9sHD8JUlrNWAxvwM86CrbmVXltEKd7zk= 42 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 43 | github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 44 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 45 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 46 | github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= 47 | github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= 48 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 49 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 50 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 51 | github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= 52 | github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 54 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 55 | github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 56 | github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= 57 | github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= 58 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 59 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 60 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 61 | github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= 62 | github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= 63 | github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= 64 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 65 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 66 | github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= 67 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 68 | github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= 69 | github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= 70 | github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= 71 | github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 72 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 73 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 74 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 75 | go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 76 | go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= 77 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 78 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM= 79 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= 80 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 81 | golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= 82 | golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= 83 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 84 | google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU= 85 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 86 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 87 | k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= 88 | k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= 89 | k8s.io/apiserver v0.26.2/go.mod h1:GHcozwXgXsPuOJ28EnQ/jXEM9QeG6HT22YxSNmpYNh8= 90 | k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= 91 | k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEBivs= 92 | k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0= 93 | k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 94 | k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 95 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 96 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 97 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 98 | tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto= 99 | tags.cncf.io/container-device-interface/specs-go v0.7.0/go.mod h1:hMAwAbMZyBLdmYqWgYcKH0F/yctNpV3P35f+/088A80= 100 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package txdb 2 | 3 | import "fmt" 4 | 5 | // SavePoint defines the syntax to create savepoints 6 | // within transaction 7 | type SavePoint interface { 8 | Create(id string) string 9 | Release(id string) string 10 | Rollback(id string) string 11 | } 12 | 13 | type defaultSavePoint struct{} 14 | 15 | func (dsp *defaultSavePoint) Create(id string) string { 16 | return fmt.Sprintf("SAVEPOINT %s", id) 17 | } 18 | func (dsp *defaultSavePoint) Release(id string) string { 19 | return fmt.Sprintf("RELEASE SAVEPOINT %s", id) 20 | } 21 | func (dsp *defaultSavePoint) Rollback(id string) string { 22 | return fmt.Sprintf("ROLLBACK TO SAVEPOINT %s", id) 23 | } 24 | 25 | // SavePointOption allows to modify the logic for 26 | // transaction save points. In such cases if your driver 27 | // does not support it, use nil. If not compatible with default 28 | // use custom. 29 | func SavePointOption(savePoint SavePoint) func(*conn) error { 30 | return func(c *conn) error { 31 | c.savePoint = savePoint 32 | return nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/bootstrap_test.go: -------------------------------------------------------------------------------- 1 | package txdb_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/testcontainers/testcontainers-go" 11 | "github.com/testcontainers/testcontainers-go/modules/mysql" 12 | "github.com/testcontainers/testcontainers-go/modules/postgres" 13 | "github.com/testcontainers/testcontainers-go/wait" 14 | ) 15 | 16 | const ( 17 | mysql_sql = `CREATE TABLE users ( 18 | id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, 19 | username VARCHAR(32) NOT NULL, 20 | email VARCHAR(255) NOT NULL, 21 | PRIMARY KEY (id), 22 | UNIQUE INDEX uniq_email (email) 23 | ) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB` 24 | 25 | psql_sql = `CREATE TABLE users ( 26 | id SERIAL PRIMARY KEY, 27 | username VARCHAR(32) NOT NULL, 28 | email VARCHAR(255) UNIQUE NOT NULL 29 | )` 30 | 31 | inserts = `INSERT INTO users (username, email) VALUES ('gopher', 'gopher@go.com'), ('john', 'john@doe.com'), ('jane', 'jane@doe.com')` 32 | 33 | testDB = "txdb_test" 34 | ) 35 | 36 | // bootstrap bootstraps the database for tests. 37 | func bootstrap(t *testing.T, driver, dsn string) { 38 | db, err := sql.Open(driver, dsn) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | switch driver { 43 | case "mysql": 44 | if _, err := db.Exec(mysql_sql); err != nil { 45 | t.Fatal(err) 46 | } 47 | case "postgres": 48 | if _, err := db.Exec(psql_sql); err != nil { 49 | t.Fatal(err) 50 | } 51 | default: 52 | panic("unrecognized driver: " + driver) 53 | } 54 | if _, err := db.Exec(inserts); err != nil { 55 | t.Fatal(err) 56 | } 57 | } 58 | 59 | func createDB(t *testing.T, driver, dsn string) { 60 | db, err := sql.Open(driver, dsn) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if _, err := db.Exec("DROP DATABASE IF EXISTS " + testDB); err != nil { 65 | t.Fatal(err) 66 | } 67 | if _, err := db.Exec("CREATE DATABASE " + testDB); err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | 72 | func startPostgres(t *testing.T) string { 73 | ctx := context.Background() 74 | 75 | postgresContainer, err := postgres.Run(ctx, "docker.io/postgres:15.2-alpine", 76 | postgres.WithDatabase(testDB), 77 | testcontainers.WithWaitStrategy( 78 | wait.ForLog("database system is ready to accept connections"). 79 | WithOccurrence(2). 80 | WithStartupTimeout(5*time.Second)), 81 | ) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | dsn, err := postgresContainer.ConnectionString(ctx) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | return strings.TrimSuffix(dsn, testDB+"?") 91 | } 92 | 93 | func startMySQL(t *testing.T) string { 94 | ctx := context.Background() 95 | 96 | mysqlContainer, err := mysql.Run(ctx, "mysql:8", 97 | mysql.WithUsername("root"), 98 | mysql.WithPassword("password"), 99 | mysql.WithDatabase(testDB), 100 | ) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | dsn, err := mysqlContainer.ConnectionString(ctx) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | return strings.TrimSuffix(dsn, testDB) 109 | } 110 | -------------------------------------------------------------------------------- /tests/db_test.go: -------------------------------------------------------------------------------- 1 | package txdb_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "reflect" 10 | "sort" 11 | "strings" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | "github.com/DATA-DOG/go-txdb" 17 | 18 | _ "github.com/go-sql-driver/mysql" 19 | _ "github.com/lib/pq" 20 | ) 21 | 22 | var txDrivers = testDrivers{ 23 | {name: "mysql_txdb", driver: "mysql", dsnEnvKey: "MYSQL_DSN", options: "multiStatements=true"}, 24 | {name: "psql_txdb", driver: "postgres", dsnEnvKey: "PSQL_DSN", options: "sslmode=disable"}, 25 | } 26 | 27 | type testDriver struct { 28 | // name is the name we use internally for the connection. 29 | name string 30 | // driver is the name registered by the driver when imported. 31 | driver string 32 | // dsnEnvKey is the name of an environment variable to fetch the DSN from. 33 | // The DSN is expected to include the name of the database, and any 34 | // necessary credentials. 35 | dsnEnvKey string 36 | // options are optional parameters appended to the DSN before connecting. 37 | options string 38 | // registered is set to true once the driver is registered, to prevent 39 | // duplicate registration. 40 | registered bool 41 | } 42 | 43 | type testDrivers []*testDriver 44 | 45 | var registerMu sync.Mutex 46 | 47 | // dsn returns the base dsn (without DB name) and the full dsn (with dbname) 48 | // for the test driver, or calls t.Skip if it is unset or disabled. 49 | func (d *testDriver) dsn(t *testing.T) (base string, full string) { 50 | t.Helper() 51 | base = os.Getenv(d.dsnEnvKey) 52 | if base == "" { 53 | t.Skipf("%s not set, skipping tests for %s", d.dsnEnvKey, d.driver) 54 | } 55 | if strings.ToLower(base) == "auto" { 56 | base = d.startTestContainer(t) 57 | } 58 | full = strings.TrimSuffix(base, "/") + "/" + testDB 59 | if d.options == "" { 60 | return base, full 61 | } 62 | return base + "?" + d.options, full + "?" + d.options 63 | } 64 | 65 | func (d *testDriver) startTestContainer(t *testing.T) string { 66 | switch d.driver { 67 | case "postgres": 68 | return startPostgres(t) 69 | case "mysql": 70 | return startMySQL(t) 71 | default: 72 | panic("cannot start testcontainer for driver " + d.driver) 73 | } 74 | } 75 | 76 | func (d *testDriver) register(t *testing.T) { 77 | t.Helper() 78 | registerMu.Lock() 79 | defer registerMu.Unlock() 80 | if !d.registered { 81 | base, full := d.dsn(t) 82 | d.registered = true 83 | createDB(t, d.driver, base) 84 | bootstrap(t, d.driver, full) 85 | txdb.Register(d.name, d.driver, full) 86 | } 87 | } 88 | 89 | // Run registers the driver, if not already registered, then calls f with the 90 | // driver name. 91 | func (d *testDriver) Run(t *testing.T, f func(t *testing.T, driver *testDriver)) { 92 | t.Helper() 93 | t.Run(d.name, func(t *testing.T) { 94 | d.register(t) 95 | f(t, d) 96 | }) 97 | } 98 | 99 | // Run iterates over the configured drivers, and calls [testDriver.Run] on each. 100 | func (d testDrivers) Run(t *testing.T, f func(t *testing.T, driver *testDriver)) { 101 | t.Helper() 102 | for _, driver := range d { 103 | driver.Run(t, f) 104 | } 105 | } 106 | 107 | // driver returns the subset of d whose driver match one of the provided names. 108 | // Useful for tests that require specific database driver capabilities. 109 | func (d testDrivers) drivers(names ...string) testDrivers { 110 | result := make(testDrivers, 0, len(d)) 111 | for _, driver := range d { 112 | for _, name := range names { 113 | if driver.driver == name { 114 | result = append(result, driver) 115 | } 116 | } 117 | } 118 | return result 119 | } 120 | 121 | func TestShouldWorkWithOpenDB(t *testing.T) { 122 | t.Parallel() 123 | for _, d := range txDrivers { 124 | d.Run(t, func(t *testing.T, driver *testDriver) { 125 | _, dsn := driver.dsn(t) 126 | db := sql.OpenDB(txdb.New(d.driver, dsn)) 127 | defer db.Close() 128 | _, err := db.Exec("SELECT 1") 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func TestShouldRunWithNestedTransaction(t *testing.T) { 137 | t.Parallel() 138 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 139 | var count int 140 | db, err := sql.Open(driver.name, "five") 141 | if err != nil { 142 | t.Fatalf("failed to open a connection: %s", err) 143 | } 144 | 145 | func(db *sql.DB) { 146 | defer db.Close() 147 | 148 | _, err = db.Exec(`INSERT INTO users (username, email) VALUES('txdb', 'txdb@test1.com')`) 149 | if err != nil { 150 | t.Fatalf("failed to insert a user: %s", err) 151 | } 152 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 153 | if err != nil { 154 | t.Fatalf("failed to count users: %s", err) 155 | } 156 | if count != 4 { 157 | t.Fatalf("expected 4 users to be in database, but got %d", count) 158 | } 159 | 160 | tx, err := db.Begin() 161 | if err != nil { 162 | t.Fatalf("failed to begin transaction: %s", err) 163 | } 164 | { 165 | _, err = tx.Exec(`INSERT INTO users (username, email) VALUES('txdb', 'txdb@test2.com')`) 166 | if err != nil { 167 | t.Fatalf("failed to insert an user: %s", err) 168 | } 169 | err = tx.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 170 | if err != nil { 171 | t.Fatalf("failed to count users: %s", err) 172 | } 173 | if count != 5 { 174 | t.Fatalf("expected 5 users to be in database, but got %d", count) 175 | } 176 | if err := tx.Rollback(); err != nil { 177 | t.Fatalf("failed to rollback transaction: %s", err) 178 | } 179 | } 180 | 181 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 182 | if err != nil { 183 | t.Fatalf("failed to count users: %s", err) 184 | } 185 | if count != 4 { 186 | t.Fatalf("expected 4 users to be in database, but got %d", count) 187 | } 188 | 189 | tx, err = db.Begin() 190 | if err != nil { 191 | t.Fatalf("failed to begin transaction: %s", err) 192 | } 193 | { 194 | _, err = tx.Exec(`INSERT INTO users (username, email) VALUES('txdb', 'txdb@test2.com')`) 195 | if err != nil { 196 | t.Fatalf("failed to insert an user: %s", err) 197 | } 198 | err = tx.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 199 | if err != nil { 200 | t.Fatalf("failed to count users: %s", err) 201 | } 202 | if count != 5 { 203 | t.Fatalf("expected 5 users to be in database, but got %d", count) 204 | } 205 | if err := tx.Commit(); err != nil { 206 | t.Fatalf("failed to commit transaction: %s", err) 207 | } 208 | } 209 | 210 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 211 | if err != nil { 212 | t.Fatalf("failed to count users: %s", err) 213 | } 214 | if count != 5 { 215 | t.Fatalf("expected 5 users to be in database, but got %d", count) 216 | } 217 | }(db) 218 | 219 | db, err = sql.Open(driver.name, "six") 220 | if err != nil { 221 | t.Fatalf("failed to reopen a mysql connection: %s", err) 222 | } 223 | func(db *sql.DB) { 224 | defer db.Close() 225 | 226 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 227 | if err != nil { 228 | t.Fatalf("failed to count users: %s", err) 229 | } 230 | if count != 3 { 231 | t.Fatalf("expected 3 users to be in database, but got %d", count) 232 | } 233 | }(db) 234 | }) 235 | } 236 | 237 | func TestShouldRunWithinTransaction(t *testing.T) { 238 | t.Parallel() 239 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 240 | var count int 241 | db, err := sql.Open(driver.name, "one") 242 | if err != nil { 243 | t.Fatalf("failed to open a connection: %s", err) 244 | } 245 | 246 | func(db *sql.DB) { 247 | defer db.Close() 248 | 249 | _, err = db.Exec(`INSERT INTO users (username, email) VALUES('txdb', 'txdb@test.com')`) 250 | if err != nil { 251 | t.Fatalf("failed to insert an user: %s", err) 252 | } 253 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 254 | if err != nil { 255 | t.Fatalf("failed to count users: %s", err) 256 | } 257 | if count != 4 { 258 | t.Fatalf("expected 4 users to be in database, but got %d", count) 259 | } 260 | }(db) 261 | 262 | db, err = sql.Open(driver.name, "two") 263 | if err != nil { 264 | t.Fatalf("failed to reopen a mysql connection: %s", err) 265 | } 266 | func(db *sql.DB) { 267 | defer db.Close() 268 | 269 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 270 | if err != nil { 271 | t.Fatalf("failed to count users: %s", err) 272 | } 273 | if count != 3 { 274 | t.Fatalf("expected 3 users to be in database, but got %d", count) 275 | } 276 | }(db) 277 | }) 278 | } 279 | 280 | func TestShouldNotHoldConnectionForRows(t *testing.T) { 281 | t.Parallel() 282 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 283 | db, err := sql.Open(driver.name, "three") 284 | if err != nil { 285 | t.Fatalf("failed to open a connection: %s", err) 286 | } 287 | defer db.Close() 288 | 289 | rows, err := db.Query("SELECT username FROM users") 290 | if err != nil { 291 | t.Fatalf("failed to query users: %s", err) 292 | } 293 | defer rows.Close() 294 | 295 | _, err = db.Exec(`INSERT INTO users(username, email) VALUES('txdb', 'txdb@test.com')`) 296 | if err != nil { 297 | t.Fatalf("failed to insert an user: %s", err) 298 | } 299 | }) 300 | } 301 | 302 | func TestShouldPerformParallelActions(t *testing.T) { 303 | t.Parallel() 304 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 305 | db, err := sql.Open(driver.name, "four") 306 | if err != nil { 307 | t.Fatalf("failed to open a connection: %s", err) 308 | } 309 | defer db.Close() 310 | 311 | wg := &sync.WaitGroup{} 312 | for i := 0; i < 4; i++ { 313 | wg.Add(1) 314 | go func(d *sql.DB, idx int) { 315 | defer wg.Done() 316 | rows, err := d.Query("SELECT username FROM users") 317 | if err != nil { 318 | t.Errorf("failed to query users: %s", err) 319 | } 320 | defer rows.Close() 321 | 322 | insertSQL := "INSERT INTO users(username, email) VALUES(?, ?)" 323 | if strings.Index(driver.name, "psql_") == 0 { 324 | insertSQL = "INSERT INTO users(username, email) VALUES($1, $2)" 325 | } 326 | username := fmt.Sprintf("parallel%d", idx) 327 | email := fmt.Sprintf("parallel%d@test.com", idx) 328 | _, err = d.Exec(insertSQL, username, email) 329 | if err != nil { 330 | t.Errorf("failed to insert an user: %s", err) 331 | } 332 | }(db, i) 333 | } 334 | wg.Wait() 335 | var count int 336 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 337 | if err != nil { 338 | t.Fatalf("failed to count users: %s", err) 339 | } 340 | if count != 7 { 341 | t.Fatalf("expected 7 users to be in database, but got %d", count) 342 | } 343 | }) 344 | } 345 | 346 | func TestShouldFailInvalidPrepareStatement(t *testing.T) { 347 | t.Parallel() 348 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 349 | db, err := sql.Open(driver.name, "fail_prepare") 350 | if err != nil { 351 | t.Fatalf("failed to open a connection: %s", err) 352 | } 353 | defer db.Close() 354 | 355 | if _, err = db.Prepare("THIS SHOULD FAIL..."); err == nil { 356 | t.Fatal("expected an error, since prepare should validate sql query, but got none") 357 | } 358 | }) 359 | } 360 | 361 | func TestShouldHandlePrepare(t *testing.T) { 362 | t.Parallel() 363 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 364 | db, err := sql.Open(driver.name, "prepare") 365 | if err != nil { 366 | t.Fatalf("failed to open a connection: %s", err) 367 | } 368 | defer db.Close() 369 | 370 | selectSQL := "SELECT email FROM users WHERE username = ?" 371 | if strings.Index(driver.name, "psql_") == 0 { 372 | selectSQL = "SELECT email FROM users WHERE username = $1" 373 | } 374 | 375 | stmt1, err := db.Prepare(selectSQL) 376 | if err != nil { 377 | t.Fatalf("could not prepare - %s", err) 378 | } 379 | 380 | insertSQL := "INSERT INTO users (username, email) VALUES(?, ?)" 381 | if strings.Index(driver.name, "psql_") == 0 { 382 | insertSQL = "INSERT INTO users (username, email) VALUES($1, $2)" 383 | } 384 | stmt2, err := db.Prepare(insertSQL) 385 | if err != nil { 386 | t.Fatalf("could not prepare - %s", err) 387 | } 388 | 389 | var email string 390 | if err = stmt1.QueryRow("jane").Scan(&email); err != nil { 391 | t.Fatalf("could not scan email - %s", err) 392 | } 393 | 394 | _, err = stmt2.Exec("mark", "mark.spencer@gmail.com") 395 | if err != nil { 396 | t.Fatalf("should have inserted user - %s", err) 397 | } 398 | }) 399 | } 400 | 401 | func TestShouldCloseRootDB(t *testing.T) { 402 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 403 | db1, err := sql.Open(driver.name, "first") 404 | if err != nil { 405 | t.Fatalf("failed to open a connection: %s", err) 406 | } 407 | defer db1.Close() 408 | 409 | stmt, err := db1.Prepare("SELECT * FROM users") 410 | if err != nil { 411 | t.Fatalf("could not prepare - %s", err) 412 | } 413 | defer stmt.Close() 414 | 415 | drv1 := db1.Driver().(*txdb.TxDriver) 416 | if drv1.DB() == nil { 417 | t.Fatalf("expected database, drv1.db: %v", drv1.DB()) 418 | } 419 | 420 | db2, err := sql.Open(driver.name, "second") 421 | if err != nil { 422 | t.Fatalf("failed to open a connection: %s", err) 423 | } 424 | defer db2.Close() 425 | 426 | stmt, err = db2.Prepare("SELECT * FROM users") 427 | if err != nil { 428 | t.Fatalf("could not prepare - %s", err) 429 | } 430 | defer stmt.Close() 431 | 432 | // Both drivers share the same database. 433 | drv2 := db2.Driver().(*txdb.TxDriver) 434 | if drv2.DB() != drv1.DB() { 435 | t.Fatalf("drv1.db=%v != drv2.db=%v", drv1.DB(), drv2.DB()) 436 | } 437 | 438 | // Database should remain open while a connection is open. 439 | if err := db1.Close(); err != nil { 440 | t.Fatalf("could not close database - %s", err) 441 | } 442 | 443 | if drv1.DB() == nil { 444 | t.Fatal("expected database, not nil") 445 | } 446 | 447 | if drv2.DB() == nil { 448 | t.Fatal("expected database ,not nil") 449 | } 450 | 451 | // Database should close after last connection is closed. 452 | if err := db2.Close(); err != nil { 453 | t.Fatalf("could not close database - %s", err) 454 | } 455 | 456 | if drv1.DB() != nil { 457 | t.Fatalf("expected closed database, not %v", drv1.DB()) 458 | } 459 | 460 | if drv2.DB() != nil { 461 | t.Fatalf("expected closed database, not %v", drv2.DB()) 462 | } 463 | }) 464 | } 465 | 466 | func TestShouldReopenAfterClose(t *testing.T) { 467 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 468 | db, err := sql.Open(driver.name, "first") 469 | if err != nil { 470 | t.Fatalf("failed to open a connection: %s", err) 471 | } 472 | defer db.Close() 473 | 474 | stmt, err := db.Prepare("SELECT * FROM users") 475 | if err != nil { 476 | t.Fatalf("could not prepare - %s", err) 477 | } 478 | defer stmt.Close() 479 | 480 | if err := db.Close(); err != nil { 481 | t.Fatalf("could not close database - %s", err) 482 | } 483 | 484 | if err := db.Ping(); err.Error() != "sql: database is closed" { 485 | t.Fatalf("expected closed database - %s", err) 486 | } 487 | 488 | db, err = sql.Open(driver.name, "second") 489 | if err != nil { 490 | t.Fatalf("failed to open a connection: %s", err) 491 | } 492 | defer db.Close() 493 | 494 | if err := db.Ping(); err != nil { 495 | t.Fatalf("failed to ping: %s", err) 496 | } 497 | }) 498 | } 499 | 500 | type canceledContext struct{} 501 | 502 | func (canceledContext) Deadline() (deadline time.Time, ok bool) { return time.Time{}, true } 503 | func (canceledContext) Done() <-chan struct{} { 504 | done := make(chan struct{}) 505 | close(done) 506 | return done 507 | } 508 | func (canceledContext) Err() error { return errors.New("canceled") } 509 | func (canceledContext) Value(key interface{}) interface{} { return nil } 510 | 511 | func TestShouldDiscardConnectionWhenClosedBecauseOfError(t *testing.T) { 512 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 513 | { 514 | db, err := sql.Open(driver.name, "first") 515 | if err != nil { 516 | t.Fatalf("failed to open a connection: %s", err) 517 | } 518 | defer db.Close() 519 | 520 | tx, err := db.Begin() 521 | defer func() { 522 | err = tx.Rollback() 523 | if err != nil { 524 | t.Fatalf("rollback err: %s", err) 525 | } 526 | }() 527 | if err != nil { 528 | t.Fatalf("failed to begin transaction err: %s", err) 529 | } 530 | 531 | // TODO: we somehow need to poison the DB connection here so that Rollback fails 532 | 533 | _, err = tx.PrepareContext(canceledContext{}, "SELECT * FROM users") 534 | if err == nil { 535 | t.Fatal("should have returned error for prepare") 536 | } 537 | } 538 | 539 | fmt.Println("Opening db...") 540 | 541 | { 542 | db, err := sql.Open(driver.name, "second") 543 | if err != nil { 544 | t.Fatalf("failed to open a connection: %s", err) 545 | } 546 | defer db.Close() 547 | 548 | if err := db.Ping(); err != nil { 549 | t.Fatalf("failed to ping: %s", err) 550 | } 551 | } 552 | }) 553 | } 554 | 555 | func TestPostgresRowsScanTypeTables(t *testing.T) { 556 | txDrivers.drivers("postgres").Run(t, func(t *testing.T, driver *testDriver) { 557 | db, err := sql.Open(driver.name, "scantype") 558 | if err != nil { 559 | t.Fatalf("psql: failed to open a postgres connection: %s", err) 560 | } 561 | defer db.Close() 562 | 563 | rows, err := db.Query("SELECT 1") 564 | if err != nil { 565 | t.Fatalf("psql: unable to execute trivial query: %v", err) 566 | } 567 | 568 | colTypes, err := rows.ColumnTypes() 569 | if err != nil { 570 | t.Fatalf("psql: unable to retrieve column types: %v", err) 571 | } 572 | 573 | int32Type := reflect.TypeOf(int32(0)) 574 | if colTypes[0].ScanType() != int32Type { 575 | t.Fatalf("psql: column scan type is %s, but should be %s", colTypes[0].ScanType().String(), int32Type.String()) 576 | } 577 | }) 578 | } 579 | 580 | func TestMysqlShouldBeAbleToLockTables(t *testing.T) { 581 | txDrivers.drivers("mysql").Run(t, func(t *testing.T, driver *testDriver) { 582 | db, err := sql.Open(driver.name, "locks") 583 | if err != nil { 584 | t.Fatalf("mysql: failed to open a mysql connection: %s", err) 585 | } 586 | defer db.Close() 587 | 588 | _, err = db.Exec("LOCK TABLE users READ") 589 | if err != nil { 590 | t.Fatalf("mysql: should be able to lock table, but got err: %v", err) 591 | } 592 | 593 | var count int 594 | err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) 595 | if err != nil { 596 | t.Fatalf("mysql: unexpected read error: %v", err) 597 | } 598 | if count != 3 { 599 | t.Fatalf("mysql: was expecting 3 users in db") 600 | } 601 | 602 | _, err = db.Exec("UNLOCK TABLES") 603 | if err != nil { 604 | t.Fatalf("mysql: should be able to unlock table, but got err: %v", err) 605 | } 606 | }) 607 | } 608 | 609 | func TestShouldGetMultiRowSet(t *testing.T) { 610 | t.Parallel() 611 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 612 | db, err := sql.Open(driver.name, "multiRows") 613 | if err != nil { 614 | t.Fatalf("failed to open a connection: %s", err) 615 | } 616 | defer db.Close() 617 | 618 | rows, err := db.QueryContext(context.Background(), "SELECT username FROM users; SELECT COUNT(*) FROM users;") 619 | if err != nil { 620 | t.Fatalf("failed to query users: %s", err) 621 | } 622 | defer rows.Close() 623 | 624 | var users []string 625 | for rows.Next() { 626 | var name string 627 | if err := rows.Scan(&name); err != nil { 628 | t.Fatalf("unexpected row scan err: %v", err) 629 | } 630 | users = append(users, name) 631 | } 632 | 633 | if !rows.NextResultSet() { 634 | t.Fatal("expected next result set") 635 | } 636 | 637 | if !rows.Next() { 638 | t.Fatal("expected next result set - row") 639 | } 640 | 641 | var count int 642 | if err := rows.Scan(&count); err != nil { 643 | t.Fatalf("unexpected row scan err: %v", err) 644 | } 645 | 646 | if count != len(users) { 647 | t.Fatal("unexpected number of users") 648 | } 649 | }) 650 | } 651 | 652 | func TestShouldBeAbleToPingWithContext(t *testing.T) { 653 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 654 | db, err := sql.Open(driver.name, "ping") 655 | if err != nil { 656 | t.Fatalf("failed to open a connection: %s", err) 657 | } 658 | defer db.Close() 659 | 660 | if err := db.PingContext(context.Background()); err != nil { 661 | t.Fatalf("%v", err) 662 | } 663 | }) 664 | } 665 | 666 | func TestShouldHandleStmtsWithoutContextPollution(t *testing.T) { 667 | t.Parallel() 668 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 669 | db, err := sql.Open(driver.name, "contextpollution") 670 | if err != nil { 671 | t.Fatalf("failed to open a connection: %s", err) 672 | } 673 | defer db.Close() 674 | 675 | insertSQL := "INSERT INTO users (username, email) VALUES(?, ?)" 676 | if strings.Index(driver.name, "psql_") == 0 { 677 | insertSQL = "INSERT INTO users (username, email) VALUES($1, $2)" 678 | } 679 | 680 | ctx1, cancel1 := context.WithCancel(context.Background()) 681 | defer cancel1() 682 | 683 | _, err = db.ExecContext(ctx1, insertSQL, "first", "first@foo.com") 684 | if err != nil { 685 | t.Fatalf("unexpected error inserting user 1: %s", err) 686 | } 687 | cancel1() 688 | 689 | ctx2, cancel2 := context.WithCancel(context.Background()) 690 | defer cancel2() 691 | 692 | _, err = db.ExecContext(ctx2, insertSQL, "second", "second@foo.com") 693 | if err != nil { 694 | t.Fatalf("unexpected error inserting user 2: %s", err) 695 | } 696 | cancel2() 697 | 698 | const selectQuery = ` 699 | select username 700 | from users 701 | where username = 'first' OR username = 'second'` 702 | 703 | rows, err := db.QueryContext(context.Background(), selectQuery) 704 | if err != nil { 705 | t.Fatalf("unexpected error querying users: %s", err) 706 | } 707 | defer rows.Close() 708 | 709 | assertRows := func(t *testing.T, rows *sql.Rows) { 710 | t.Helper() 711 | 712 | var users []string 713 | for rows.Next() { 714 | var user string 715 | err := rows.Scan(&user) 716 | if err != nil { 717 | t.Errorf("unexpected scan failure: %s", err) 718 | continue 719 | } 720 | users = append(users, user) 721 | } 722 | sort.Strings(users) 723 | 724 | wanted := []string{"first", "second"} 725 | 726 | if len(users) != 2 { 727 | t.Fatalf("invalid users received; want=%v\tgot=%v", wanted, users) 728 | } 729 | for i, want := range wanted { 730 | if got := users[i]; want != got { 731 | t.Errorf("invalid user; want=%s\tgot=%s", want, got) 732 | } 733 | } 734 | } 735 | 736 | assertRows(t, rows) 737 | 738 | ctx3, cancel3 := context.WithCancel(context.Background()) 739 | defer cancel3() 740 | 741 | stmt, err := db.PrepareContext(ctx3, selectQuery) 742 | if err != nil { 743 | t.Fatalf("unexpected error preparing stmt: %s", err) 744 | } 745 | 746 | rows, err = stmt.QueryContext(context.TODO()) 747 | if err != nil { 748 | t.Fatalf("unexpected error in stmt querying users: %s", err) 749 | } 750 | defer rows.Close() 751 | 752 | assertRows(t, rows) 753 | }) 754 | } 755 | 756 | // https://github.com/DATA-DOG/go-txdb/issues/49 757 | func TestIssue49(t *testing.T) { 758 | t.Parallel() 759 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 760 | db, err := sql.Open(driver.name, "rollback") 761 | if err != nil { 762 | t.Fatalf("failed to open a connection: %s", err) 763 | } 764 | defer db.Close() 765 | 766 | // do a query prior to starting a nested transaction to 767 | // reproduce the error 768 | var count int 769 | err = db.QueryRow("SELECT COUNT(id) FROM users").Scan(&count) 770 | if err != nil { 771 | t.Fatalf("prepared statement count err %v", err) 772 | } 773 | if count != 3 { 774 | t.Logf("Count not 3: %d", count) 775 | t.FailNow() 776 | } 777 | 778 | // start a nested transaction 779 | tx, err := db.Begin() 780 | if err != nil { 781 | t.Fatalf("failed to start transaction: %s", err) 782 | } 783 | // need a prepared statement to reproduce the error 784 | insertSQL := "INSERT INTO users (username, email) VALUES(?, ?)" 785 | if strings.Index(driver.name, "psql_") == 0 { 786 | insertSQL = "INSERT INTO users (username, email) VALUES($1, $2)" 787 | } 788 | stmt, err := tx.Prepare(insertSQL) 789 | if err != nil { 790 | t.Fatalf("failed to prepare named statement: %s", err) 791 | } 792 | 793 | // try to insert already existing username/email 794 | _, err = stmt.Exec("gopher", "gopher@go.com") 795 | if err == nil { 796 | t.Fatal("double insert?") 797 | } 798 | // The insert failed, so we need to close the prepared statement 799 | err = stmt.Close() 800 | if err != nil { 801 | t.Fatalf("error closing prepared statement: %s", err) 802 | } 803 | // rollback the transaction now that it has failed 804 | err = tx.Rollback() 805 | if err != nil { 806 | t.Logf("failed rollback of failed transaction: %s", err) 807 | t.FailNow() 808 | } 809 | }) 810 | } 811 | 812 | func TestShouldRunWithHeavyWork(t *testing.T) { 813 | t.Parallel() 814 | 815 | testFn := func(t *testing.T, db *sql.DB) { 816 | t.Helper() 817 | 818 | ctx, cancel := context.WithCancel(context.Background()) 819 | defer cancel() 820 | row, err := db.QueryContext(ctx, "SELECT 1 from HeavyWork") 821 | if err != nil { 822 | t.Fatalf("failed to query users: %s", err) 823 | } 824 | if err := row.Close(); err != nil { 825 | t.Fatalf("failed to close rows: %s", err) 826 | } 827 | } 828 | 829 | txDrivers.Run(t, func(t *testing.T, driver *testDriver) { 830 | db, err := sql.Open(driver.name, "HeavyWork") 831 | if err != nil { 832 | t.Fatalf("failed to open a connection: %s", err) 833 | } 834 | defer db.Close() 835 | 836 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS HeavyWork (id INT, name VARCHAR(255))") 837 | if err != nil { 838 | t.Fatalf("failed to create table: %s", err) 839 | } 840 | 841 | for i := 0; i < 10000; i++ { 842 | testFn(t, db) 843 | } 844 | }) 845 | } 846 | -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DATA-DOG/go-txdb/tests 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/DATA-DOG/go-txdb v0.2.0 7 | github.com/go-sql-driver/mysql v1.9.0 8 | github.com/lib/pq v1.10.9 9 | github.com/testcontainers/testcontainers-go v0.35.0 10 | github.com/testcontainers/testcontainers-go/modules/mysql v0.35.0 11 | github.com/testcontainers/testcontainers-go/modules/postgres v0.35.0 12 | ) 13 | 14 | require ( 15 | dario.cat/mergo v1.0.0 // indirect 16 | filippo.io/edwards25519 v1.1.0 // indirect 17 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 18 | github.com/Microsoft/go-winio v0.6.2 // indirect 19 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 20 | github.com/containerd/containerd v1.7.18 // indirect 21 | github.com/containerd/log v0.1.0 // indirect 22 | github.com/containerd/platforms v0.2.1 // indirect 23 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/distribution/reference v0.6.0 // indirect 26 | github.com/docker/docker v27.1.1+incompatible // indirect 27 | github.com/docker/go-connections v0.5.0 // indirect 28 | github.com/docker/go-units v0.5.0 // indirect 29 | github.com/felixge/httpsnoop v1.0.4 // indirect 30 | github.com/go-logr/logr v1.4.1 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/go-ole/go-ole v1.2.6 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/google/uuid v1.6.0 // indirect 35 | github.com/klauspost/compress v1.17.4 // indirect 36 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 37 | github.com/magiconair/properties v1.8.7 // indirect 38 | github.com/moby/docker-image-spec v1.3.1 // indirect 39 | github.com/moby/patternmatcher v0.6.0 // indirect 40 | github.com/moby/sys/sequential v0.5.0 // indirect 41 | github.com/moby/sys/user v0.1.0 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/morikuni/aec v1.0.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.1.0 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/pmezard/go-difflib v1.0.0 // indirect 48 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 49 | github.com/shirou/gopsutil/v3 v3.23.12 // indirect 50 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 51 | github.com/sirupsen/logrus v1.9.3 // indirect 52 | github.com/stretchr/testify v1.9.0 // indirect 53 | github.com/tklauser/go-sysconf v0.3.12 // indirect 54 | github.com/tklauser/numcpus v0.6.1 // indirect 55 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 56 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 57 | go.opentelemetry.io/otel v1.24.0 // indirect 58 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 59 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 60 | golang.org/x/crypto v0.31.0 // indirect 61 | golang.org/x/sys v0.28.0 // indirect 62 | gopkg.in/yaml.v3 v3.0.1 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /tests/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 6 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 7 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 8 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 9 | github.com/DATA-DOG/go-txdb v0.2.0 h1:p1VAEZGN0U58Z5efRbI9mI6fDhcMn2+hV1sPBeOp/A8= 10 | github.com/DATA-DOG/go-txdb v0.2.0/go.mod h1:Dqk6PhlGpMk1JZ3n8sjybgBLcW69nuijArOMubFCXM0= 11 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 12 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 13 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 14 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 | github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= 16 | github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= 17 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 18 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 19 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 20 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 21 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 22 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 23 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 24 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 29 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 30 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 31 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 32 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 33 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 34 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 35 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 36 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 37 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 38 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 39 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 40 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 41 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 42 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 43 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 44 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 45 | github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= 46 | github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= 47 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 48 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 49 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 51 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 52 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 53 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 54 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 55 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 56 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 57 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 58 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 59 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 60 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 61 | github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= 62 | github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 63 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 64 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 65 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 66 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 67 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 68 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 69 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 70 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 71 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 72 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 73 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 74 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 75 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 76 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 77 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 78 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 79 | github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM= 80 | github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64= 81 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 82 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 83 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 84 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 85 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 86 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 87 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 88 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 89 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 90 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 91 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 92 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 93 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 94 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 95 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 96 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 97 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 98 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 99 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 100 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 102 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 103 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 104 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 105 | github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= 106 | github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= 107 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 108 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 109 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 110 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 111 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 112 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 115 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 116 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 117 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 118 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 120 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 121 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 122 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 123 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 124 | github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= 125 | github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= 126 | github.com/testcontainers/testcontainers-go/modules/mysql v0.35.0 h1:9voGAf+1KxC0ck/XtrC/AUrkr74SSGpQRBp0O851B3Y= 127 | github.com/testcontainers/testcontainers-go/modules/mysql v0.35.0/go.mod h1:rxKSkFpc5XZtG00prjqPfobuMgt5EpFEOrzZgYdOX0c= 128 | github.com/testcontainers/testcontainers-go/modules/postgres v0.35.0 h1:eEGx9kYzZb2cNhRbBrNOCL/YPOM7+RMJiy3bB+ie0/I= 129 | github.com/testcontainers/testcontainers-go/modules/postgres v0.35.0/go.mod h1:hfH71Mia/WWLBgMD2YctYcMlfsbnT0hflweL1dy8Q4s= 130 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 131 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 132 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 133 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 134 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 135 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 136 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 137 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 138 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 139 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 140 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 141 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 142 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 143 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 144 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 145 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 146 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 147 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 148 | go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= 149 | go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= 150 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 151 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 152 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 153 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 154 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 155 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 156 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 157 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 158 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 159 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 160 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 161 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 162 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 163 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 164 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 165 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 166 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 167 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 171 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 172 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 182 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 183 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 184 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 185 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 186 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 187 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 188 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 189 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 190 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 191 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 192 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 193 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 194 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 195 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 196 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 198 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 200 | google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0= 201 | google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= 202 | google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= 203 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= 204 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= 205 | google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= 206 | google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= 207 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 208 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 212 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 213 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 214 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 215 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 216 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 217 | --------------------------------------------------------------------------------