db-tx.md
August 31, 2026 · View on GitHub
DbTx is the transactional connection context supplied by Transactor#transact. It extends DbCon (so every Frag/Repo operation that needs DbCon also accepts DbTx) and adds savepoint-based nested transaction support. You never construct a DbTx directly; the Transactor creates one and supplies it as a given context to the block passed to transact. The connection is always closed when the outermost block exits, whether it commits, rolls back, or throws.
Key properties:
- Transactional context — A
DbTxvalue in scope guarantees the underlying JDBC connection has auto-commit disabled. - Commit-on-success / rollback-on-failure — The outermost
Transactor.transactcommits on normal return and rolls back on any uncaught exception (with suppressed rollback failures). - Savepoint-based nesting — Inner blocks reuse the same connection via SQL savepoints (
SAVEPOINT/RELEASE SAVEPOINT/ROLLBACK TO SAVEPOINT). transact(isolation, readOnly)— The two-argTransactor.transactoverload sets isolation level andreadOnlybefore disabling auto-commit;DbTxnested blocks inherit those settings on the same connection.
The structural declaration of DbTx is:
trait DbTx extends DbCon {
def savepoint(name: String): Unit
def release(name: String): Unit
def rollbackTo(name: String): Unit
def currentDepth: Int
private[sql] def currentDepth_=(depth: Int): Unit
// inherited from DbCon
def connection: DbConnection
def dialect: SqlDialect
def logger: SqlLogger
}
Usage
The following example opens a transaction via Transactor#transact, accesses all three context members, and combines a Repo CRUD operation with a hand-written Frag query — both of which accept DbTx transparently in place of DbCon:
import zio.blocks.sql._
import zio.blocks.schema.Schema
case class User(id: Int, name: String, email: String)
object User {
implicit val schema: Schema[User] = Schema.derived
}
val repo = Repo.derived[User, Int]("users", "id", _.id)
val tx = JdbcTransactor.fromUrl("jdbc:sqlite::memory:", SqlDialect.SQLite)
// On normal return: transaction commits and connection closes.
// On any exception: transaction rolls back, then the exception propagates.
tx.transact {
// All three context members are accessible via summon[DbTx]
val conn: DbConnection = summon[DbTx].connection // managed JDBC connection — do not close manually
val d: SqlDialect = summon[DbTx].dialect
val log: SqlLogger = summon[DbTx].logger
// Repo and Frag operations accept DbTx because DbTx extends DbCon
repo.table.createTable(summon[DbTx].dialect).update
repo.insert(User(1, "Alice", "alice@example.com"))
repo.insert(User(2, "Bob", "bob@example.com"))
val all: List[User] = repo.all
val custom: List[User] =
sql"SELECT id, name, email FROM users WHERE name LIKE ${"A%"}".query[User]
(all, custom)
}
Nested Transactions via Savepoints
Nested transactions reuse the same underlying JDBC connection via SQL savepoints. The DbTx given in scope exposes an extension transact and the transactNested helpers:
import zio.blocks.sql._
val transactor = JdbcTransactor.fromUrl("jdbc:sqlite::memory:", SqlDialect.SQLite)
// Savepoint-based nesting — same connection, isolated rollback
transactor.transact {
sql"INSERT INTO t VALUES (1)".update
// Inner block runs inside SAVEPOINT zib_tx_1
summon[DbTx].transact {
sql"INSERT INTO t VALUES (2)".update
}
// Equivalent using the `using` helper
// DbTx.transactNested { sql"INSERT INTO t VALUES (3)".update }
// transactNested { sql"INSERT INTO t VALUES (4)".update }
}
Savepoint names are zib_tx_1 .. zib_tx_N where N is the nesting depth tracked in currentDepth. On success the savepoint is released via RELEASE SAVEPOINT; on failure it is rolled back via ROLLBACK TO SAVEPOINT and the exception is rethrown (with any rollback failure added as suppressed). Depth is decremented in finally, so sibling nested blocks reuse the same name sequence without leaking savepoints. savepoint/release/rollbackTo are also available directly for manual control and validate identifiers via SqlIdentifier to prevent injection.
:::caution
Only the outermost Transactor.transact issues a real COMMIT/ROLLBACK. Inner summon[DbTx].transact blocks are savepoint-scoped — outer commit still decides the final persistence of all work, including inner blocks that succeeded.
:::