[
  {
    "path": ".github/workflows/test.yml",
    "content": "on: [push, pull_request]\nname: Test\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go-version: [\"1.21\", \"1.22\"]\n\n    services:\n      postgres:\n        image: postgres:13-alpine\n        ports:\n          - 5432:5432\n        env:\n          POSTGRES_USER: sqalx\n          POSTGRES_PASSWORD: sqalx\n      mysql:\n        image: mysql:8.0\n        ports:\n          - 3306:3306\n        env:\n          MYSQL_ROOT_PASSWORD: sqalx\n          MYSQL_USER: sqalx\n          MYSQL_PASSWORD: sqalx\n          MYSQL_DATABASE: sqalx\n\n    steps:\n      - name: Install Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Test\n        run: make test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016 Heetch\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "POSTGRESQL_DATASOURCE ?= postgresql://sqalx:sqalx@localhost:5432/sqalx?sslmode=disable\nMYSQL_DATASOURCE ?= sqalx:sqalx@tcp(localhost:3306)/sqalx\nSQLITE_DATASOURCE ?= :memory:\n\n.PHONY: test\n\ntest:\n\tPOSTGRESQL_DATASOURCE=\"$(POSTGRESQL_DATASOURCE)\" \\\n\tMYSQL_DATASOURCE=\"$(MYSQL_DATASOURCE)\" \\\n\tSQLITE_DATASOURCE=\"$(SQLITE_DATASOURCE)\" \\\n\tgo test -v -cover -race -timeout=1m ./... && echo OK || (echo FAIL && exit 1)\n"
  },
  {
    "path": "README.md",
    "content": "\n# :warning: Warning: This repository is considered inactive and no change will be made to it except for security updates.\n\n# sqalx\n\n[![GoDoc](https://godoc.org/github.com/heetch/sqalx?status.svg)](https://godoc.org/github.com/heetch/sqalx)\n[![Go Report Card](https://goreportcard.com/badge/github.com/heetch/sqalx)](https://goreportcard.com/report/github.com/heetch/sqalx)\n\nsqalx (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.\nWith sqalx you can easily create reusable and composable functions that can be called within or out of transactions and that can create transactions themselves.\n\n## Getting started\n\n```sh\n$ go get github.com/heetch/sqalx\n```\n\n### Import sqalx\n\n```go\nimport \"github.com/heetch/sqalx\"\n```\n\n### Usage\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"github.com/heetch/sqalx\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/lib/pq\"\n)\n\nfunc main() {\n\t// Connect to PostgreSQL with sqlx.\n\tdb, err := sqlx.Connect(\"postgres\", \"user=foo dbname=bar sslmode=disable\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tdefer db.Close()\n\n\t// Pass the db to sqalx.\n\t// It returns a sqalx.Node. A Node is a wrapper around sqlx.DB or sqlx.Tx.\n\tnode, err := sqalx.New(db)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\terr = createUser(node)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc createUser(node sqalx.Node) error {\n\t// Exec a query\n\t_, _ = node.Exec(\"INSERT INTO ....\") // you can use a node as if it were a *sqlx.DB or a *sqlx.Tx\n\n\t// Let's create a transaction.\n\t// A transaction is also a sqalx.Node.\n\ttx, err := node.Beginx()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback()\n\n\t_, _ = tx.Exec(\"UPDATE ...\")\n\n\t// Now we call another function and pass it the transaction.\n\terr = updateGroups(tx)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn tx.Commit()\n}\n\nfunc updateGroups(node sqalx.Node) error {\n\t// Notice we are creating a new transaction.\n\t// This would normally cause a dead lock without sqalx.\n\ttx, err := node.Beginx()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback()\n\n\t_, _ = tx.Exec(\"INSERT ...\")\n\t_, _ = tx.Exec(\"UPDATE ...\")\n\t_, _ = tx.Exec(\"DELETE ...\")\n\n\treturn tx.Commit()\n}\n```\n\n### PostgreSQL Savepoints\n\nWhen 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.\n\n```go\nnode, err := sqalx.New(db, sqalx.SavePoint(true))\n```\n\n## Issue\nPlease open an issue if you encounter any problem.\n\n## Development\nsqalx 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.\n\n### Running all tests\nTo 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.\n\n### Running specific tests\nTo test against the Postgres instance be sure to export the following DSN:\n\n```sh\nexport POSTGRESQL_DATASOURCE=\"postgresql://sqalx:sqalx@localhost:5432/sqalx?sslmode=disable\"\n```\n\nTo test against the MySQL instance be sure to export the following DSN:\n\n```sh\nexport MYSQL_DATASOURCE=\"sqalx:sqalx@tcp(localhost:3306)/sqalx\"\n```\n\nTo test against SQlite export the following DSN:\n\n```sh\nexport SQLITE_DATASOURCE=\":memory:\"\n```\n\n_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`.\n\n## License\n The library is released under the MIT license. See [LICENSE](LICENSE) file.\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.6'\n\nservices:\n    postgres:\n        image: postgres:9.6-alpine\n        ports:\n            - 5432:5432\n        environment:\n            - POSTGRES_USER=sqalx\n            - POSTGRES_PASSWORD=sqalx\n\n    mysql:\n        image: mysql:8.0 # intel only\n        # image: mysql/mysql-server:8.0 # mac M1 preview\n        ports:\n            - 3306:3306\n        environment:\n            - MYSQL_ROOT_PASSWORD=sqalx\n            - MYSQL_USER=sqalx\n            - MYSQL_PASSWORD=sqalx\n            - MYSQL_DATABASE=sqalx\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/heetch/sqalx\n\ngo 1.21\n\nrequire (\n\tgithub.com/DATA-DOG/go-sqlmock v1.5.2\n\tgithub.com/go-sql-driver/mysql v1.7.1\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/jackc/pgx/v5 v5.5.4\n\tgithub.com/jmoiron/sqlx v1.3.5\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/mattn/go-sqlite3 v1.14.22\n\tgithub.com/stretchr/testify v1.9.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rogpeppe/go-internal v1.12.0 // indirect\n\tgolang.org/x/crypto v0.21.0 // indirect\n\tgolang.org/x/sync v0.1.0 // indirect\n\tgolang.org/x/text v0.14.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=\ngithub.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=\ngithub.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=\ngithub.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=\ngithub.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=\ngithub.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=\ngithub.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=\ngithub.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=\ngithub.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngolang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=\ngolang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=\ngolang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "sqalx.go",
    "content": "package sqalx\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nvar (\n\t// ErrNotInTransaction is returned when using Commit\n\t// outside of a transaction.\n\tErrNotInTransaction = errors.New(\"not in transaction\")\n\n\t// ErrIncompatibleOption is returned when using an option incompatible\n\t// with the selected driver.\n\tErrIncompatibleOption = errors.New(\"incompatible option\")\n)\n\n// A Node is a database driver that can manage nested transactions.\ntype Node interface {\n\tDriver\n\n\t// Close the underlying sqlx connection.\n\tClose() error\n\t// Begin a new transaction.\n\tBeginx() (Node, error)\n\t// Begin a new transaction using the provided context and options.\n\t// Note that the provided parameters are only used when opening a new transaction,\n\t// not on nested ones.\n\tBeginTxx(ctx context.Context, opts *sql.TxOptions) (Node, error)\n\t// Rollback the associated transaction.\n\tRollback() error\n\t// Commit the assiociated transaction.\n\tCommit() error\n\t// Tx returns the underlying transaction.\n\tTx() *sqlx.Tx\n}\n\n// A Driver can query the database. It can either be a *sqlx.DB or a *sqlx.Tx\n// and therefore is limited to the methods they have in common.\ntype Driver interface {\n\tsqlx.Execer\n\tsqlx.ExecerContext\n\tsqlx.Queryer\n\tsqlx.QueryerContext\n\tsqlx.Preparer\n\tsqlx.PreparerContext\n\tBindNamed(query string, arg interface{}) (string, []interface{}, error)\n\tDriverName() string\n\tGet(dest interface{}, query string, args ...interface{}) error\n\tGetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error\n\tMustExec(query string, args ...interface{}) sql.Result\n\tMustExecContext(ctx context.Context, query string, args ...interface{}) sql.Result\n\tNamedExec(query string, arg interface{}) (sql.Result, error)\n\tNamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error)\n\tNamedQuery(query string, arg interface{}) (*sqlx.Rows, error)\n\tPrepareNamed(query string) (*sqlx.NamedStmt, error)\n\tPrepareNamedContext(ctx context.Context, query string) (*sqlx.NamedStmt, error)\n\tPreparex(query string) (*sqlx.Stmt, error)\n\tPreparexContext(ctx context.Context, query string) (*sqlx.Stmt, error)\n\tQueryRow(string, ...interface{}) *sql.Row\n\tQueryRowContext(context.Context, string, ...interface{}) *sql.Row\n\tRebind(query string) string\n\tSelect(dest interface{}, query string, args ...interface{}) error\n\tSelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error\n}\n\n// New creates a new Node with the given DB.\nfunc New(db *sqlx.DB, options ...Option) (Node, error) {\n\tn := node{\n\t\tdb:     db,\n\t\tDriver: db,\n\t}\n\n\tfor _, opt := range options {\n\t\terr := opt(&n)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &n, nil\n}\n\n// NewFromTransaction creates a new Node from the given transaction.\nfunc NewFromTransaction(tx *sqlx.Tx, options ...Option) (Node, error) {\n\tn := node{\n\t\ttx:     tx,\n\t\tDriver: tx,\n\t}\n\n\tfor _, opt := range options {\n\t\terr := opt(&n)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &n, nil\n}\n\n// Connect to a database.\nfunc Connect(driverName, dataSourceName string, options ...Option) (Node, error) {\n\tdb, err := sqlx.Connect(driverName, dataSourceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnode, err := New(db, options...)\n\tif err != nil {\n\t\t// the connection has been opened within this function, we must close it\n\t\t// on error.\n\t\tdb.Close()\n\t\treturn nil, err\n\t}\n\n\treturn node, nil\n}\n\ntype node struct {\n\tDriver\n\tdb               *sqlx.DB\n\ttx               *sqlx.Tx\n\tsavePointID      string\n\tsavePointEnabled bool\n\tnested           bool\n}\n\nfunc (n *node) Close() error {\n\treturn n.db.Close()\n}\n\nfunc (n node) Beginx() (Node, error) {\n\treturn n.BeginTxx(context.Background(), nil)\n}\n\nfunc (n node) BeginTxx(ctx context.Context, opts *sql.TxOptions) (Node, error) {\n\tvar err error\n\n\tswitch {\n\tcase n.tx == nil:\n\t\t// new actual transaction\n\t\tn.tx, err = n.db.BeginTxx(ctx, opts)\n\t\tn.Driver = n.tx\n\tcase n.savePointEnabled:\n\t\t// already in a transaction: using savepoints\n\t\tn.nested = true\n\t\t// savepoints name must start with a char and cannot contain dashes (-)\n\t\tn.savePointID = \"sp_\" + strings.Replace(uuid.NewString(), \"-\", \"_\", -1)\n\t\t_, err = n.tx.Exec(\"SAVEPOINT \" + n.savePointID)\n\tdefault:\n\t\t// already in a transaction: reusing current transaction\n\t\tn.nested = true\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &n, nil\n}\n\nfunc (n *node) Rollback() error {\n\tif n.tx == nil {\n\t\treturn nil\n\t}\n\n\tvar err error\n\n\tif n.savePointEnabled && n.savePointID != \"\" {\n\t\t_, err = n.tx.Exec(\"ROLLBACK TO SAVEPOINT \" + n.savePointID)\n\t} else if !n.nested {\n\t\terr = n.tx.Rollback()\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn.tx = nil\n\tn.Driver = nil\n\n\treturn nil\n}\n\nfunc (n *node) Commit() error {\n\tif n.tx == nil {\n\t\treturn ErrNotInTransaction\n\t}\n\n\tvar err error\n\n\tif n.savePointID != \"\" {\n\t\t_, err = n.tx.Exec(\"RELEASE SAVEPOINT \" + n.savePointID)\n\t} else if !n.nested {\n\t\terr = n.tx.Commit()\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn.tx = nil\n\tn.Driver = nil\n\n\treturn nil\n}\n\n// Tx returns the underlying transaction.\nfunc (n *node) Tx() *sqlx.Tx {\n\treturn n.tx\n}\n\n// Option to configure sqalx\ntype Option func(*node) error\n\n// SavePoint option enables PostgreSQL and SQLite Savepoints for nested\n// transactions.\nfunc SavePoint(enabled bool) Option {\n\treturn func(n *node) error {\n\t\tdriverName := n.Driver.DriverName()\n\t\tif enabled && driverName != \"postgres\" && driverName != \"pgx\" && driverName != \"pgx/v5\" && driverName != \"sqlite3\" && driverName != \"mysql\" {\n\t\t\treturn ErrIncompatibleOption\n\t\t}\n\t\tn.savePointEnabled = enabled\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "sqalx_test.go",
    "content": "package sqalx_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\tsqlmock \"github.com/DATA-DOG/go-sqlmock\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t\"github.com/heetch/sqalx\"\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/lib/pq\"\n\t_ \"github.com/mattn/go-sqlite3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc prepareDB(t *testing.T, driverName string) (*sqlx.DB, sqlmock.Sqlmock, func()) {\n\tdb, mock, err := sqlmock.New()\n\trequire.NoError(t, err)\n\n\treturn sqlx.NewDb(db, driverName), mock, func() {\n\t\tdb.Close()\n\t}\n}\n\nfunc TestSqalxConnectPostgreSQL(t *testing.T) {\n\tdataSource := os.Getenv(\"POSTGRESQL_DATASOURCE\")\n\tif dataSource == \"\" {\n\t\tt.Log(\"skipping due to blank POSTGRESQL_DATASOURCE\")\n\t\tt.Skip()\n\t\treturn\n\t}\n\n\ttestSqalxConnect(t, \"postgres\", dataSource)\n\ttestSqalxConnect(t, \"postgres\", dataSource, sqalx.SavePoint(true))\n}\nfunc TestSqalxConnectPGX(t *testing.T) {\n\tdataSource := os.Getenv(\"POSTGRESQL_DATASOURCE\")\n\tif dataSource == \"\" {\n\t\tt.Log(\"skipping due to blank POSTGRESQL_DATASOURCE\")\n\t\tt.Skip()\n\t\treturn\n\t}\n\n\ttestSqalxConnect(t, \"pgx\", dataSource)\n\ttestSqalxConnect(t, \"pgx\", dataSource, sqalx.SavePoint(true))\n}\n\nfunc TestSqalxConnectSqlite(t *testing.T) {\n\tdataSource := os.Getenv(\"SQLITE_DATASOURCE\")\n\tif dataSource == \"\" {\n\t\tt.Skip()\n\t\treturn\n\t}\n\n\ttestSqalxConnect(t, \"sqlite3\", dataSource)\n\ttestSqalxConnect(t, \"sqlite3\", dataSource, sqalx.SavePoint(true))\n}\n\nfunc TestSqalxConnectMySQL(t *testing.T) {\n\tdataSource := os.Getenv(\"MYSQL_DATASOURCE\")\n\tif dataSource == \"\" {\n\t\tt.Log(\"skipping due to blank MYSQL_DATASOURCE\")\n\t\tt.Skip()\n\t\treturn\n\t}\n\n\ttestSqalxConnect(t, \"mysql\", dataSource)\n\ttestSqalxConnect(t, \"mysql\", dataSource, sqalx.SavePoint(true))\n}\n\nfunc testSqalxConnect(t *testing.T, driverName, dataSource string, options ...sqalx.Option) {\n\tnode, err := sqalx.Connect(driverName, dataSource, options...)\n\trequire.NoError(t, err)\n\n\terr = node.Close()\n\trequire.NoError(t, err)\n}\n\nfunc TestSqalxTransactionViolations(t *testing.T) {\n\tnode, err := sqalx.New(nil)\n\trequire.NoError(t, err)\n\n\trequire.Panics(t, func() {\n\t\t//nolint:errcheck // the intended panic makes error checking irrelevant\n\t\tnode.Exec(\"UPDATE products SET views = views + 1\")\n\t})\n\n\trequire.Panics(t, func() {\n\t\t//nolint:errcheck // the intended panic makes error checking irrelevant\n\t\tnode.Beginx()\n\t})\n\n\t// calling Rollback after a transaction is closed does nothing\n\terr = node.Rollback()\n\trequire.NoError(t, err)\n\n\terr = node.Commit()\n\trequire.Equal(t, err, sqalx.ErrNotInTransaction)\n}\n\nfunc TestSqalxSimpleQuery(t *testing.T) {\n\tdb, mock, cleanup := prepareDB(t, \"mock\")\n\tdefer cleanup()\n\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\n\tnode, err := sqalx.New(db)\n\trequire.NoError(t, err)\n\n\t_, err = node.Exec(\"UPDATE products SET views = views + 1\")\n\trequire.NoError(t, err)\n}\n\nfunc TestSqalxTopLevelTransaction(t *testing.T) {\n\tdb, mock, cleanup := prepareDB(t, \"mock\")\n\tdefer cleanup()\n\tvar err error\n\n\tmock.ExpectBegin()\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tmock.ExpectCommit()\n\n\tnode, err := sqalx.New(db)\n\trequire.NoError(t, err)\n\n\tnode, err = node.Beginx()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, node)\n\tdefer func() {\n\t\terr = node.Rollback()\n\t\trequire.NoError(t, err)\n\t}()\n\n\t_, err = node.Exec(\"UPDATE products SET views = views + 1\")\n\trequire.NoError(t, err)\n\n\terr = node.Commit()\n\trequire.NoError(t, err)\n}\n\nfunc TestSqalxNestedTransactions(t *testing.T) {\n\ttestSqalxNestedTransactions(t, \"mock\", false)\n}\n\nfunc TestSqalxNestedTransactionsWithSavePoint(t *testing.T) {\n\tfor _, driver := range []string{\n\t\t\"postgres\",\n\t\t\"pgx\",\n\t\t\"sqlite3\",\n\t\t\"mysql\",\n\t} {\n\t\tt.Run(driver, func(t *testing.T) {\n\t\t\ttestSqalxNestedTransactions(t, driver, true)\n\t\t})\n\t}\n}\n\nfunc testSqalxNestedTransactions(t *testing.T, driverName string, testSavePoint bool) {\n\tdb, mock, cleanup := prepareDB(t, driverName)\n\tdefer cleanup()\n\n\trequire.Equal(t, driverName, db.DriverName())\n\n\tvar err error\n\tconst query = \"UPDATE products SET views = views + 1\"\n\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tmock.ExpectBegin()\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tif testSavePoint {\n\t\tmock.ExpectExec(\"SAVEPOINT\").WillReturnResult(sqlmock.NewResult(1, 1))\n\t}\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tif testSavePoint {\n\t\tmock.ExpectExec(\"ROLLBACK TO SAVEPOINT\").WillReturnResult(sqlmock.NewResult(1, 1))\n\t}\n\tif testSavePoint {\n\t\tmock.ExpectExec(\"SAVEPOINT\").WillReturnResult(sqlmock.NewResult(1, 1))\n\t}\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tif testSavePoint {\n\t\tmock.ExpectExec(\"RELEASE SAVEPOINT\").WillReturnResult(sqlmock.NewResult(1, 1))\n\t}\n\tmock.ExpectCommit()\n\n\tnode, err := sqalx.New(db, sqalx.SavePoint(testSavePoint))\n\trequire.NoError(t, err)\n\n\t_, err = node.Exec(query)\n\trequire.NoError(t, err)\n\n\tn1, err := node.Beginx()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, n1)\n\n\t_, err = n1.Exec(query)\n\trequire.NoError(t, err)\n\n\tn1_1, err := n1.Beginx()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, n1_1)\n\n\t_, err = n1_1.Exec(query)\n\trequire.NoError(t, err)\n\n\terr = n1_1.Rollback()\n\trequire.NoError(t, err)\n\n\terr = n1_1.Commit()\n\trequire.Equal(t, sqalx.ErrNotInTransaction, err)\n\n\tn1_1, err = n1.BeginTxx(context.Background(), nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, n1_1)\n\n\t_, err = n1_1.Exec(query)\n\trequire.NoError(t, err)\n\n\terr = n1_1.Commit()\n\trequire.NoError(t, err)\n\n\terr = n1_1.Commit()\n\trequire.Equal(t, sqalx.ErrNotInTransaction, err)\n\n\terr = n1_1.Rollback()\n\trequire.NoError(t, err)\n\n\terr = n1.Commit()\n\trequire.NoError(t, err)\n}\n\nfunc TestSqalxFromTransaction(t *testing.T) {\n\tdb, mock, cleanup := prepareDB(t, \"mock\")\n\tdefer cleanup()\n\n\tmock.ExpectBegin()\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tmock.ExpectExec(\"UPDATE products\").WillReturnResult(sqlmock.NewResult(1, 1))\n\tmock.ExpectRollback()\n\n\ttx, err := db.Beginx()\n\trequire.NoError(t, err)\n\n\tnode, err := sqalx.NewFromTransaction(tx)\n\trequire.NoError(t, err)\n\n\t_, err = node.Exec(\"UPDATE products SET views = views + 1\")\n\trequire.NoError(t, err)\n\n\tntx, err := node.Beginx()\n\trequire.NoError(t, err)\n\t_, err = ntx.Exec(\"UPDATE products SET views = views + 1\")\n\trequire.NoError(t, err)\n\n\terr = ntx.Rollback()\n\trequire.NoError(t, err)\n\n\terr = node.Rollback()\n\trequire.NoError(t, err)\n}\n"
  }
]