Skip to main content
Version: 0.7.0

Transactions

QUARK exposes callback-style transactions for the common case and manual transactions when the calling code needs full control.

Callback transactions

The callback style commits when the function returns nil and rolls back when it returns an error.

err := client.Tx(ctx, func(tx *quark.Tx) error {
user := User{Name: "Charlie", Email: "charlie@example.com"}
if err := quark.ForTx[User](ctx, tx).Create(&user); err != nil {
return err
}

order := Order{UserID: user.ID, Total: 42}
return quark.ForTx[Order](ctx, tx).Create(&order)
})

Callback transactions also roll back safely when a panic is intercepted.

Savepoints

err := client.Tx(ctx, func(tx *quark.Tx) error {
if err := tx.Savepoint("before_optional_work"); err != nil {
return err
}

if err := runOptionalWork(ctx, tx); err != nil {
return tx.RollbackTo("before_optional_work")
}

return nil
})

Savepoint support depends on the underlying database engine.

Manual transactions

tx, err := client.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

if err := quark.ForTx[User](ctx, tx).Create(&user); err != nil {
return err
}

return tx.Commit()

Manual transactions are useful when transaction lifetime is owned by a larger workflow or framework integration.

Transaction isolation levels

import "database/sql"

tx, err := client.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
})

Available levels depend on the database engine:

LevelPostgreSQLMySQLSQLiteMSSQL
LevelReadUncommitted
LevelReadCommitted
LevelRepeatableRead
LevelSerializable

Batch operations in transactions

UpdateBatch internally wraps all updates in a single transaction. For scenarios where you need CreateBatch and DeleteBatch to share a transaction, use the manual style with ForTx:

err := client.Tx(ctx, func(tx *quark.Tx) error {
// Bulk insert new records
if err := quark.ForTx[Order](ctx, tx).CreateBatch(newOrders); err != nil {
return err
}

// Remove cancelled orders atomically
_, err := quark.ForTx[Order](ctx, tx).DeleteBatch(cancelledIDs)
return err
})

Error handling and retry

Callback transactions automatically rollback on any non-nil error return. For transient failures (e.g. deadlocks), wrap in a retry loop:

var err error
for attempt := 0; attempt < 3; attempt++ {
err = client.Tx(ctx, func(tx *quark.Tx) error {
// ...operations...
return nil
})
if err == nil {
break
}
if !isRetriable(err) {
break
}
}

Read-only transactions

Mark transactions as read-only to let the engine optimize for non-mutating queries:

tx, err := client.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
defer tx.Rollback()

users, err := quark.ForTx[User](ctx, tx).Where("active", "=", true).List()
_ = tx.Commit()