Skip to content

Transactions

gerpo does not invent its own transaction layer — it works with the Tx returned by the adapter. Tx is propagated to repositories through context.Context, so multiple repositories can share the same transaction just by sharing the same ctx.

Basic flow — manual

tx, err := adapter.BeginTx(ctx)
if err != nil {
    return err
}
defer tx.RollbackUnlessCommitted() // safety net: rolls back if Commit wasn't called

txCtx := gerpo.WithTx(ctx, tx)     // inject tx into ctx

if err := userRepo.Insert(txCtx, u); err != nil {
    return err                     // defer will roll back
}
if _, err := userRepo.Update(txCtx, u, whereByID); err != nil {
    return err
}

return tx.Commit()

Any Repository method invoked with txCtx — or a context derived from it — runs against the transaction, regardless of which repository is called. A single WithTx covers userRepo, orderRepo, itemRepo at once.

Higher-level form — gerpo.RunInTx

For the common "do some work in a transaction, commit on success, rollback on error" shape:

err := gerpo.RunInTx(ctx, adapter, func(ctx context.Context) error {
    if err := orderRepo.Insert(ctx, order); err != nil {
        return err
    }
    for _, item := range items {
        if err := itemRepo.Insert(ctx, &item); err != nil {
            return err
        }
    }
    return nil
})

RunInTx begins the transaction, injects it into the ctx it passes to fn, and commits / rolls back based on the error returned from fn. A panic inside fn is propagated after RollbackUnlessCommitted runs.

Tx methods

Method Effect
Commit() error Commits; subsequent Rollback* calls become no-ops
Rollback() error Explicit rollback
RollbackUnlessCommitted() error Safe defer: rolls back only if Commit wasn't called
ExecContext/QueryContext Raw SQL — useful when you need to bypass the repo

Isolation

Isolation is controlled by the driver; gerpo does not set a level. PostgreSQL defaults to Read Committed. For SERIALIZABLE/REPEATABLE READ, open the transaction directly via the adapter's ExecContext (BEGIN ISOLATION LEVEL …), or pass options via the driver's BeginTx (pgx accepts pgx.TxOptions).

Combining a transaction with an AfterInsert/AfterUpdate hook is how gerpo lets you express user-land one-to-many relations — the hook inserts children through their own repository, both the parent and the children land in the same tx, any failure rolls everything back. The pattern lives in the hooks page: Hooks → Cascading related rows.

Common pitfall: multiple calls without a transaction

repo.Insert(ctx, order)       // on one pool connection
repo.Insert(ctx, items...)    // may land on a different connection; not atomic

If atomicity matters — wrap in one tx and inject through gerpo.WithTx.

One adapter per context

gerpo.WithTx stores the transaction in ctx without remembering which adapter produced it. If you accidentally pass a ctx carrying a tx from adapter A to a repository built on adapter B, the tx will be used anyway — and the driver on the other end will reject the alien connection. In practice every app has one adapter per process, so this is a theoretical concern; be cautious if you mix adapters in the same process.

Partial rollback: savepoints

gerpo does not expose a dedicated SAVEPOINT API today. If you need nested rollbacks, issue them via raw SQL on the tx:

_, _ = tx.ExecContext(ctx, "SAVEPOINT sp_one")
// ... some work that may fail ...
_, _ = tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT sp_one")
// or on success:
_, _ = tx.ExecContext(ctx, "RELEASE SAVEPOINT sp_one")

Roadmap

A first-class savepoint API is listed in TODO.md. If you need it — please open an issue describing your use case so we can shape the API around real requirements.