Repository: heetch/sqalx Branch: master Commit: b907ab07d270 Files: 10 Total size: 23.8 KB Directory structure: gitextract_wsc_ajh1/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── sqalx.go └── sqalx_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ on: [push, pull_request] name: Test jobs: test: runs-on: ubuntu-latest strategy: matrix: go-version: ["1.21", "1.22"] services: postgres: image: postgres:13-alpine ports: - 5432:5432 env: POSTGRES_USER: sqalx POSTGRES_PASSWORD: sqalx mysql: image: mysql:8.0 ports: - 3306:3306 env: MYSQL_ROOT_PASSWORD: sqalx MYSQL_USER: sqalx MYSQL_PASSWORD: sqalx MYSQL_DATABASE: sqalx steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - name: Test run: make test ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Heetch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ POSTGRESQL_DATASOURCE ?= postgresql://sqalx:sqalx@localhost:5432/sqalx?sslmode=disable MYSQL_DATASOURCE ?= sqalx:sqalx@tcp(localhost:3306)/sqalx SQLITE_DATASOURCE ?= :memory: .PHONY: test test: POSTGRESQL_DATASOURCE="$(POSTGRESQL_DATASOURCE)" \ MYSQL_DATASOURCE="$(MYSQL_DATASOURCE)" \ SQLITE_DATASOURCE="$(SQLITE_DATASOURCE)" \ go test -v -cover -race -timeout=1m ./... && echo OK || (echo FAIL && exit 1) ================================================ FILE: README.md ================================================ # :warning: Warning: This repository is considered inactive and no change will be made to it except for security updates. # sqalx [![GoDoc](https://godoc.org/github.com/heetch/sqalx?status.svg)](https://godoc.org/github.com/heetch/sqalx) [![Go Report Card](https://goreportcard.com/badge/github.com/heetch/sqalx)](https://goreportcard.com/report/github.com/heetch/sqalx) sqalx (pronounced 'scale-x') is a library built on top of [sqlx](https://github.com/jmoiron/sqlx) that allows to seamlessly create nested transactions and to avoid thinking about whether or not a function is called within a transaction. With sqalx you can easily create reusable and composable functions that can be called within or out of transactions and that can create transactions themselves. ## Getting started ```sh $ go get github.com/heetch/sqalx ``` ### Import sqalx ```go import "github.com/heetch/sqalx" ``` ### Usage ```go package main import ( "log" "github.com/heetch/sqalx" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) func main() { // Connect to PostgreSQL with sqlx. db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable") if err != nil { log.Fatal(err) } defer db.Close() // Pass the db to sqalx. // It returns a sqalx.Node. A Node is a wrapper around sqlx.DB or sqlx.Tx. node, err := sqalx.New(db) if err != nil { log.Fatal(err) } err = createUser(node) if err != nil { log.Fatal(err) } } func createUser(node sqalx.Node) error { // Exec a query _, _ = node.Exec("INSERT INTO ....") // you can use a node as if it were a *sqlx.DB or a *sqlx.Tx // Let's create a transaction. // A transaction is also a sqalx.Node. tx, err := node.Beginx() if err != nil { return err } defer tx.Rollback() _, _ = tx.Exec("UPDATE ...") // Now we call another function and pass it the transaction. err = updateGroups(tx) if err != nil { return nil } return tx.Commit() } func updateGroups(node sqalx.Node) error { // Notice we are creating a new transaction. // This would normally cause a dead lock without sqalx. tx, err := node.Beginx() if err != nil { return err } defer tx.Rollback() _, _ = tx.Exec("INSERT ...") _, _ = tx.Exec("UPDATE ...") _, _ = tx.Exec("DELETE ...") return tx.Commit() } ``` ### PostgreSQL Savepoints When using the PostgreSQL driver, an option can be passed to `New` to enable the use of PostgreSQL [Savepoints](https://www.postgresql.org/docs/8.1/static/sql-savepoint.html) for nested transactions. ```go node, err := sqalx.New(db, sqalx.SavePoint(true)) ``` ## Issue Please open an issue if you encounter any problem. ## Development sqalx is covered by a go test suite. In order to test against specific databases we include a docker-compose file that runs Postgres and MySQL. ### Running all tests To run the tests, first run `docker-compose up` to run both Postgres and MySQL in locally-exposed docker images. Then run your tests via `make test` which sets up the above described data sources and runs all tests. ### Running specific tests To test against the Postgres instance be sure to export the following DSN: ```sh export POSTGRESQL_DATASOURCE="postgresql://sqalx:sqalx@localhost:5432/sqalx?sslmode=disable" ``` To test against the MySQL instance be sure to export the following DSN: ```sh export MYSQL_DATASOURCE="sqalx:sqalx@tcp(localhost:3306)/sqalx" ``` To test against SQlite export the following DSN: ```sh export SQLITE_DATASOURCE=":memory:" ``` _Note:_ If you are developing on an M1 Mac you will need to use the officially supported by Oracle image rather than the default `mysql:tag` image. It is commented out in `docker-compose.yml`. ## License The library is released under the MIT license. See [LICENSE](LICENSE) file. ================================================ FILE: docker-compose.yml ================================================ version: '3.6' services: postgres: image: postgres:9.6-alpine ports: - 5432:5432 environment: - POSTGRES_USER=sqalx - POSTGRES_PASSWORD=sqalx mysql: image: mysql:8.0 # intel only # image: mysql/mysql-server:8.0 # mac M1 preview ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORD=sqalx - MYSQL_USER=sqalx - MYSQL_PASSWORD=sqalx - MYSQL_DATABASE=sqalx ================================================ FILE: go.mod ================================================ module github.com/heetch/sqalx go 1.21 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/go-sql-driver/mysql v1.7.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.5.4 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.22 github.com/stretchr/testify v1.9.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: sqalx.go ================================================ package sqalx import ( "context" "database/sql" "errors" "strings" "github.com/google/uuid" "github.com/jmoiron/sqlx" ) var ( // ErrNotInTransaction is returned when using Commit // outside of a transaction. ErrNotInTransaction = errors.New("not in transaction") // ErrIncompatibleOption is returned when using an option incompatible // with the selected driver. ErrIncompatibleOption = errors.New("incompatible option") ) // A Node is a database driver that can manage nested transactions. type Node interface { Driver // Close the underlying sqlx connection. Close() error // Begin a new transaction. Beginx() (Node, error) // Begin a new transaction using the provided context and options. // Note that the provided parameters are only used when opening a new transaction, // not on nested ones. BeginTxx(ctx context.Context, opts *sql.TxOptions) (Node, error) // Rollback the associated transaction. Rollback() error // Commit the assiociated transaction. Commit() error // Tx returns the underlying transaction. Tx() *sqlx.Tx } // A Driver can query the database. It can either be a *sqlx.DB or a *sqlx.Tx // and therefore is limited to the methods they have in common. type Driver interface { sqlx.Execer sqlx.ExecerContext sqlx.Queryer sqlx.QueryerContext sqlx.Preparer sqlx.PreparerContext BindNamed(query string, arg interface{}) (string, []interface{}, error) DriverName() string Get(dest interface{}, query string, args ...interface{}) error GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error MustExec(query string, args ...interface{}) sql.Result MustExecContext(ctx context.Context, query string, args ...interface{}) sql.Result NamedExec(query string, arg interface{}) (sql.Result, error) NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error) NamedQuery(query string, arg interface{}) (*sqlx.Rows, error) PrepareNamed(query string) (*sqlx.NamedStmt, error) PrepareNamedContext(ctx context.Context, query string) (*sqlx.NamedStmt, error) Preparex(query string) (*sqlx.Stmt, error) PreparexContext(ctx context.Context, query string) (*sqlx.Stmt, error) QueryRow(string, ...interface{}) *sql.Row QueryRowContext(context.Context, string, ...interface{}) *sql.Row Rebind(query string) string Select(dest interface{}, query string, args ...interface{}) error SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } // New creates a new Node with the given DB. func New(db *sqlx.DB, options ...Option) (Node, error) { n := node{ db: db, Driver: db, } for _, opt := range options { err := opt(&n) if err != nil { return nil, err } } return &n, nil } // NewFromTransaction creates a new Node from the given transaction. func NewFromTransaction(tx *sqlx.Tx, options ...Option) (Node, error) { n := node{ tx: tx, Driver: tx, } for _, opt := range options { err := opt(&n) if err != nil { return nil, err } } return &n, nil } // Connect to a database. func Connect(driverName, dataSourceName string, options ...Option) (Node, error) { db, err := sqlx.Connect(driverName, dataSourceName) if err != nil { return nil, err } node, err := New(db, options...) if err != nil { // the connection has been opened within this function, we must close it // on error. db.Close() return nil, err } return node, nil } type node struct { Driver db *sqlx.DB tx *sqlx.Tx savePointID string savePointEnabled bool nested bool } func (n *node) Close() error { return n.db.Close() } func (n node) Beginx() (Node, error) { return n.BeginTxx(context.Background(), nil) } func (n node) BeginTxx(ctx context.Context, opts *sql.TxOptions) (Node, error) { var err error switch { case n.tx == nil: // new actual transaction n.tx, err = n.db.BeginTxx(ctx, opts) n.Driver = n.tx case n.savePointEnabled: // already in a transaction: using savepoints n.nested = true // savepoints name must start with a char and cannot contain dashes (-) n.savePointID = "sp_" + strings.Replace(uuid.NewString(), "-", "_", -1) _, err = n.tx.Exec("SAVEPOINT " + n.savePointID) default: // already in a transaction: reusing current transaction n.nested = true } if err != nil { return nil, err } return &n, nil } func (n *node) Rollback() error { if n.tx == nil { return nil } var err error if n.savePointEnabled && n.savePointID != "" { _, err = n.tx.Exec("ROLLBACK TO SAVEPOINT " + n.savePointID) } else if !n.nested { err = n.tx.Rollback() } if err != nil { return err } n.tx = nil n.Driver = nil return nil } func (n *node) Commit() error { if n.tx == nil { return ErrNotInTransaction } var err error if n.savePointID != "" { _, err = n.tx.Exec("RELEASE SAVEPOINT " + n.savePointID) } else if !n.nested { err = n.tx.Commit() } if err != nil { return err } n.tx = nil n.Driver = nil return nil } // Tx returns the underlying transaction. func (n *node) Tx() *sqlx.Tx { return n.tx } // Option to configure sqalx type Option func(*node) error // SavePoint option enables PostgreSQL and SQLite Savepoints for nested // transactions. func SavePoint(enabled bool) Option { return func(n *node) error { driverName := n.Driver.DriverName() if enabled && driverName != "postgres" && driverName != "pgx" && driverName != "pgx/v5" && driverName != "sqlite3" && driverName != "mysql" { return ErrIncompatibleOption } n.savePointEnabled = enabled return nil } } ================================================ FILE: sqalx_test.go ================================================ package sqalx_test import ( "context" "os" "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" _ "github.com/go-sql-driver/mysql" "github.com/heetch/sqalx" _ "github.com/jackc/pgx/v5/stdlib" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" ) func prepareDB(t *testing.T, driverName string) (*sqlx.DB, sqlmock.Sqlmock, func()) { db, mock, err := sqlmock.New() require.NoError(t, err) return sqlx.NewDb(db, driverName), mock, func() { db.Close() } } func TestSqalxConnectPostgreSQL(t *testing.T) { dataSource := os.Getenv("POSTGRESQL_DATASOURCE") if dataSource == "" { t.Log("skipping due to blank POSTGRESQL_DATASOURCE") t.Skip() return } testSqalxConnect(t, "postgres", dataSource) testSqalxConnect(t, "postgres", dataSource, sqalx.SavePoint(true)) } func TestSqalxConnectPGX(t *testing.T) { dataSource := os.Getenv("POSTGRESQL_DATASOURCE") if dataSource == "" { t.Log("skipping due to blank POSTGRESQL_DATASOURCE") t.Skip() return } testSqalxConnect(t, "pgx", dataSource) testSqalxConnect(t, "pgx", dataSource, sqalx.SavePoint(true)) } func TestSqalxConnectSqlite(t *testing.T) { dataSource := os.Getenv("SQLITE_DATASOURCE") if dataSource == "" { t.Skip() return } testSqalxConnect(t, "sqlite3", dataSource) testSqalxConnect(t, "sqlite3", dataSource, sqalx.SavePoint(true)) } func TestSqalxConnectMySQL(t *testing.T) { dataSource := os.Getenv("MYSQL_DATASOURCE") if dataSource == "" { t.Log("skipping due to blank MYSQL_DATASOURCE") t.Skip() return } testSqalxConnect(t, "mysql", dataSource) testSqalxConnect(t, "mysql", dataSource, sqalx.SavePoint(true)) } func testSqalxConnect(t *testing.T, driverName, dataSource string, options ...sqalx.Option) { node, err := sqalx.Connect(driverName, dataSource, options...) require.NoError(t, err) err = node.Close() require.NoError(t, err) } func TestSqalxTransactionViolations(t *testing.T) { node, err := sqalx.New(nil) require.NoError(t, err) require.Panics(t, func() { //nolint:errcheck // the intended panic makes error checking irrelevant node.Exec("UPDATE products SET views = views + 1") }) require.Panics(t, func() { //nolint:errcheck // the intended panic makes error checking irrelevant node.Beginx() }) // calling Rollback after a transaction is closed does nothing err = node.Rollback() require.NoError(t, err) err = node.Commit() require.Equal(t, err, sqalx.ErrNotInTransaction) } func TestSqalxSimpleQuery(t *testing.T) { db, mock, cleanup := prepareDB(t, "mock") defer cleanup() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) node, err := sqalx.New(db) require.NoError(t, err) _, err = node.Exec("UPDATE products SET views = views + 1") require.NoError(t, err) } func TestSqalxTopLevelTransaction(t *testing.T) { db, mock, cleanup := prepareDB(t, "mock") defer cleanup() var err error mock.ExpectBegin() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() node, err := sqalx.New(db) require.NoError(t, err) node, err = node.Beginx() require.NoError(t, err) require.NotNil(t, node) defer func() { err = node.Rollback() require.NoError(t, err) }() _, err = node.Exec("UPDATE products SET views = views + 1") require.NoError(t, err) err = node.Commit() require.NoError(t, err) } func TestSqalxNestedTransactions(t *testing.T) { testSqalxNestedTransactions(t, "mock", false) } func TestSqalxNestedTransactionsWithSavePoint(t *testing.T) { for _, driver := range []string{ "postgres", "pgx", "sqlite3", "mysql", } { t.Run(driver, func(t *testing.T) { testSqalxNestedTransactions(t, driver, true) }) } } func testSqalxNestedTransactions(t *testing.T, driverName string, testSavePoint bool) { db, mock, cleanup := prepareDB(t, driverName) defer cleanup() require.Equal(t, driverName, db.DriverName()) var err error const query = "UPDATE products SET views = views + 1" mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectBegin() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) if testSavePoint { mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1)) } mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) if testSavePoint { mock.ExpectExec("ROLLBACK TO SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1)) } if testSavePoint { mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1)) } mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) if testSavePoint { mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1)) } mock.ExpectCommit() node, err := sqalx.New(db, sqalx.SavePoint(testSavePoint)) require.NoError(t, err) _, err = node.Exec(query) require.NoError(t, err) n1, err := node.Beginx() require.NoError(t, err) require.NotNil(t, n1) _, err = n1.Exec(query) require.NoError(t, err) n1_1, err := n1.Beginx() require.NoError(t, err) require.NotNil(t, n1_1) _, err = n1_1.Exec(query) require.NoError(t, err) err = n1_1.Rollback() require.NoError(t, err) err = n1_1.Commit() require.Equal(t, sqalx.ErrNotInTransaction, err) n1_1, err = n1.BeginTxx(context.Background(), nil) require.NoError(t, err) require.NotNil(t, n1_1) _, err = n1_1.Exec(query) require.NoError(t, err) err = n1_1.Commit() require.NoError(t, err) err = n1_1.Commit() require.Equal(t, sqalx.ErrNotInTransaction, err) err = n1_1.Rollback() require.NoError(t, err) err = n1.Commit() require.NoError(t, err) } func TestSqalxFromTransaction(t *testing.T) { db, mock, cleanup := prepareDB(t, "mock") defer cleanup() mock.ExpectBegin() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectRollback() tx, err := db.Beginx() require.NoError(t, err) node, err := sqalx.NewFromTransaction(tx) require.NoError(t, err) _, err = node.Exec("UPDATE products SET views = views + 1") require.NoError(t, err) ntx, err := node.Beginx() require.NoError(t, err) _, err = ntx.Exec("UPDATE products SET views = views + 1") require.NoError(t, err) err = ntx.Rollback() require.NoError(t, err) err = node.Rollback() require.NoError(t, err) }