sqlxtransaction

sqlxtransaction runs a function inside a sqlx transaction, with the same guarded commit and rollback handling as the database/sql variant.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/sqlxtransaction"

The sqltransaction pattern against jmoiron/sqlx, so the callback receives a *sqlx.Tx.

err := sqlxtransaction.Exec(ctx, db, func(ctx context.Context, tx *sqlx.Tx) error {
    return tx.SelectContext(ctx, &users, query, arg)
})

Why a separate package

sqlx.Tx embeds sql.Tx but is a distinct type, and its extra methods are the reason to be using sqlx at all. A callback typed func(context.Context, *sql.Tx) error cannot call SelectContext, GetContext, NamedExecContext or StructScan, which is most of what sqlx is for.

Keeping them separate also keeps the dependency separate. sqltransaction reaches no external module, so a service not using sqlx does not pull it in. Go resolves dependencies per package, so importing one of these two does not bring the other. See /docs/dependency-footprint/.

Identical semantics

The transaction handling matches the database/sql version exactly:

  1. Begin through DB.BeginTxx.
  2. Run the function with the *sqlx.Tx.
  3. Commit on success.
  4. Roll back if the function failed or commit was not reached.

And the same three rollback guards: skipped after a successful commit, sql.ErrTxDone ignored during the deferred rollback, and a genuine rollback failure joined with the current error rather than replacing it.

Matching behaviour matters for a codebase using both, which is common when one part uses struct scanning and another does raw work. The two behave the same, so the transaction semantics do not depend on which package a given repository method happens to use.

Isolation levels

ExecWithOptions takes *sql.TxOptions, as the other variant does. Exec uses the driver default, which differs between MySQL and PostgreSQL, so be explicit where it matters.

What sqlx gives you inside the callback

The methods that make the wrapper worth having:

err := sqlxtransaction.Exec(ctx, db, func(ctx context.Context, tx *sqlx.Tx) error {
    if err := tx.SelectContext(ctx, &rows, "SELECT ... WHERE id = ?", id); err != nil {
        return err
    }

    _, err := tx.NamedExecContext(ctx, "UPDATE t SET name = :name WHERE id = :id", rec)

    return err
})

SelectContext scans into a slice of structs, GetContext into one, and NamedExecContext binds :name parameters from a struct or map. All of it participates in the transaction, and all of it rolls back together.

Dependencies

Reaches github.com/jmoiron/sqlx.