Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds parameter to commit transaction on failure #11

Merged
merged 2 commits into from
Jun 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ val withDbEx: ZIO[Database, DbException, Long] = Database.transactionOrWiden(zio
val zioEx: ZIO[Connection, java.io.IOException, Long] = ???
val withEx: ZIO[Database, Exception, Long] = Database.transactionOrWiden(zioEx)

// You can also commit even on a failure (only rollbacking on a defect). Useful if you're using the failure channel for short-circuiting!
val commitOnFailure: ZIO[Database, String, Long] = Database.transactionOrDie(zio, commitOnFailure = true)

// And if you're actually not interested in a transaction, you can just auto-commit all queries.
val zioAutoCommit: ZIO[Database, String, Long] = Database.autoCommitOrDie(zio)
```
Expand Down Expand Up @@ -232,6 +235,10 @@ val result2: ZIO[Database with ZEnv, E, A] = Database.transactionOrDieR[ZEnv](zi
val result3: ZIO[Database with ZEnv, Exception, A] = Database.transactionOrWidenR[ZEnv](zio)
```

Finally, all the `transaction` methods take an optional argument `commitOnFailure` (defaults to `false`).
If `true`, the transaction will be commited on a failure (the `E` part in `ZIO[R, E, A]`), and will still be rollbacked on a defect.
Obviously, this argument does not exist on the `autoCommit` methods.



### Database module configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ abstract class DatabaseModuleBase[Connection, Dbs <: DatabaseOps.ServiceOps[Conn
type Database = Has[Dbs]

private[tranzactio]
override def transactionRFull[R <: Has[_], E, A](zio: ZIO[R with Connection, E, A]): ZIO[Has[Dbs] with R, Either[DbException, E], A] = {
override def transactionRFull[R <: Has[_], E, A](
zio: ZIO[R with Connection, E, A],
commitOnFailure: Boolean = false
): ZIO[Has[Dbs] with R, Either[DbException, E], A] = {
ZIO.accessM { db: Has[Dbs] =>
db.get[Dbs].transactionRFull[R, E, A](zio).provideSome[R] { r =>
db.get[Dbs].transactionRFull[R, E, A](zio, commitOnFailure).provideSome[R] { r =>
val env = r ++ Has(()) // needed for the compiler
env
}
Expand Down
33 changes: 17 additions & 16 deletions src/main/scala/io/github/gaelrenoux/tranzactio/DatabaseOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package io.github.gaelrenoux.tranzactio

import zio.{Has, UIO, ZIO}

// TODO: Needs partial application for the environment, especially when applied on the service level

/** Operations for a Database, based on a few atomic operations. Can be used both by the actual DB service, or by the DB
* component where a Database is required in the resulting ZIO.
* @tparam R0 Environment needed to run the operations.
Expand All @@ -16,7 +14,10 @@ trait DatabaseOps[Connection, R0] {
protected def mixHasUnit(r0: R0): R0 with Has[Unit]

/** Method that should be implemented by subclasses, to provide the connection. Full (not partial) application. */
private[tranzactio] def transactionRFull[R <: Has[_], E, A](zio: ZIO[R with Connection, E, A]): ZIO[R with R0, Either[DbException, E], A]
private[tranzactio] def transactionRFull[R <: Has[_], E, A](
zio: ZIO[R with Connection, E, A],
commitOnFailure: Boolean = false
): ZIO[R with R0, Either[DbException, E], A]

/** Provides that ZIO with a Connection. A transaction will be opened before any actions in the ZIO, and closed
* after. It will commit only if the ZIO succeeds, and rollback otherwise. Failures in the initial ZIO will be
Expand All @@ -26,25 +27,25 @@ trait DatabaseOps[Connection, R0] {
new TransactionRPartiallyApplied[R, Connection, R0](this)

/** As `transactionR`, where the only needed environment is the connection. */
final def transaction[E, A](zio: ZIO[Connection, E, A]): ZIO[R0, Either[DbException, E], A] =
transactionRFull[Has[Unit], E, A](zio).provideSome(mixHasUnit)
final def transaction[E, A](zio: ZIO[Connection, E, A], commitOnFailure: Boolean = false): ZIO[R0, Either[DbException, E], A] =
transactionRFull[Has[Unit], E, A](zio, commitOnFailure).provideSome(mixHasUnit)

/** As `transactionR`, but exceptions are simply widened to a common failure type. The resulting failure type is a
* superclass of both DbException and the error type of the inital ZIO. */
final def transactionOrWidenR[R <: Has[_]]: TransactionOrWidenRPartiallyApplied[R, Connection, R0] =
new TransactionOrWidenRPartiallyApplied[R, Connection, R0](this)

/** As `transactionOrWiden`, where the only needed environment is the connection. */
final def transactionOrWiden[E >: DbException, A](zio: ZIO[Connection, E, A]): ZIO[R0, E, A] =
transaction[E, A](zio).mapError(_.fold(identity, identity))
final def transactionOrWiden[E >: DbException, A](zio: ZIO[Connection, E, A], commitOnFailure: Boolean = false): ZIO[R0, E, A] =
transaction[E, A](zio, commitOnFailure).mapError(_.fold(identity, identity))

/** As `transactionR`, but errors when handling the connections are treated as defects instead of failures. */
final def transactionOrDieR[R <: Has[_]]: TransactionOrDieRPartiallyApplied[R, Connection, R0] =
new TransactionOrDieRPartiallyApplied[R, Connection, R0](this)

/** As `transactionOrDieR`, where the only needed environment is the connection. */
final def transactionOrDie[E, A](zio: ZIO[Connection, E, A]): ZIO[R0, E, A] =
transaction[E, A](zio).flatMapError(dieOnLeft)
final def transactionOrDie[E, A](zio: ZIO[Connection, E, A], commitOnFailure: Boolean = false): ZIO[R0, E, A] =
transaction[E, A](zio, commitOnFailure).flatMapError(dieOnLeft)


/** Method that should be implemented by subclasses, to provide the connection. Full (not partial) application. */
Expand All @@ -62,7 +63,7 @@ trait DatabaseOps[Connection, R0] {

/** As `autoCommitR`, but exceptions are simply widened to a common failure type. The resulting failure type is a
* superclass of both DbException and the error type of the inital ZIO. */
final def autoCommitOrWidenR[R <: Has[_]]: AutoCommitOrWidenRPartiallyApplied[R, Connection, R0] =
final def autoCommitOrWidenR[R <: Has[_]]: AutoCommitOrWidenRPartiallyApplied[R, Connection, R0] =
new AutoCommitOrWidenRPartiallyApplied[R, Connection, R0](this)

/** As `autoCommitOrWidenR`, where the only needed environment is the connection. */
Expand Down Expand Up @@ -92,18 +93,18 @@ object DatabaseOps {
}

private[tranzactio] final class TransactionRPartiallyApplied[R <: Has[_], Connection, R0](val parent: DatabaseOps[Connection, R0]) extends AnyVal {
def apply[E, A](zio: ZIO[R with Connection, E, A]): ZIO[R with R0, Either[DbException, E], A] =
parent.transactionRFull[R, E, A](zio)
def apply[E, A](zio: ZIO[R with Connection, E, A], commitOnFailure: Boolean = false): ZIO[R with R0, Either[DbException, E], A] =
parent.transactionRFull[R, E, A](zio, commitOnFailure)
}

private[tranzactio] final class TransactionOrWidenRPartiallyApplied[R <: Has[_], Connection, R0](val parent: DatabaseOps[Connection, R0]) extends AnyVal {
def apply[E >: DbException, A](zio: ZIO[R with Connection, E, A]): ZIO[R with R0, E, A] =
parent.transactionRFull[R, E, A](zio).mapError(_.fold(identity, identity))
def apply[E >: DbException, A](zio: ZIO[R with Connection, E, A], commitOnFailure: Boolean = false): ZIO[R with R0, E, A] =
parent.transactionRFull[R, E, A](zio, commitOnFailure).mapError(_.fold(identity, identity))
}

private[tranzactio] final class TransactionOrDieRPartiallyApplied[R <: Has[_], Connection, R0](val parent: DatabaseOps[Connection, R0]) extends AnyVal {
def apply[E, A](zio: ZIO[R with Connection, E, A]): ZIO[R with R0, E, A] =
parent.transactionRFull[R, E, A](zio).flatMapError(dieOnLeft)
def apply[E, A](zio: ZIO[R with Connection, E, A], commitOnFailure: Boolean = false): ZIO[R with R0, E, A] =
parent.transactionRFull[R, E, A](zio, commitOnFailure).flatMapError(dieOnLeft)
}

private[tranzactio] final class AutoCommitRPartiallyApplied[R <: Has[_], Connection, R0](val parent: DatabaseOps[Connection, R0]) extends AnyVal {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ abstract class DatabaseServiceBase[Connection <: Has[_] : Tag](connectionSource:

def connectionFromJdbc(connection: JdbcConnection): ZIO[Any, Nothing, Connection]

private[tranzactio] override def transactionRFull[R <: Has[_], E, A](zio: ZIO[R with Connection, E, A]): ZIO[R, Either[DbException, E], A] = {
private[tranzactio] override def transactionRFull[R <: Has[_], E, A](
zio: ZIO[R with Connection, E, A],
commitOnFailure: Boolean = false
): ZIO[R, Either[DbException, E], A] = {
for {
r <- ZIO.environment[R]
a <- openConnection.mapError(Left(_)).bracket(closeConnection(_).orDie) { c: JdbcConnection =>
Expand All @@ -25,7 +28,7 @@ abstract class DatabaseServiceBase[Connection <: Has[_] : Tag](connectionSource:
zio.mapError(Right(_)).provide(env)
}
.tapBoth(
_ => rollbackConnection(c).mapError(Left(_)),
_ => if (commitOnFailure) commitConnection(c).mapError(Left(_)) else rollbackConnection(c).mapError(Left(_)),
_ => commitConnection(c).mapError(Left(_))
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,75 @@ trait DatabaseOpsCompileTest {

val serviceChecks: Unit = {

/* transaction */
/* transactionR */
val _: ZIO[Environment, Either[DbException, String], Int] =
serviceOperations.transactionR[Environment](z[Connection with Environment, String])
val _: ZIO[Environment, Either[DbException, String], Int] =
serviceOperations.transactionR[Environment](z[Connection with Environment, String], commitOnFailure = true)

/* transaction */
val _: ZIO[Any, Either[DbException, String], Int] =
serviceOperations.transaction(z[Connection, String])
val _: ZIO[Any, Either[DbException, String], Int] =
serviceOperations.transaction(z[Connection, String], commitOnFailure = true)

/* transactionOrWidenR */
val _: ZIO[Environment, Exception, Int] =
serviceOperations.transactionOrWidenR[Environment](z[Connection with Environment, IllegalArgumentException])
val _: ZIO[Environment, DbException, Int] =
serviceOperations.transactionOrWidenR[Environment](z[Connection with Environment, DbException])
val _: ZIO[Environment, Exception, Int] =
serviceOperations.transactionOrWidenR[Environment](z[Connection with Environment, IllegalArgumentException], commitOnFailure = true)
val _: ZIO[Environment, DbException, Int] =
serviceOperations.transactionOrWidenR[Environment](z[Connection with Environment, DbException], commitOnFailure = true)

/* transactionOrWiden */
val _: ZIO[Any, Exception, Int] =
serviceOperations.transactionOrWiden(z[Connection, IllegalArgumentException])
val _: ZIO[Any, DbException, Int] =
serviceOperations.transactionOrWiden(z[Connection, DbException])
val _: ZIO[Any, Exception, Int] =
serviceOperations.transactionOrWiden(z[Connection, IllegalArgumentException], commitOnFailure = true)
val _: ZIO[Any, DbException, Int] =
serviceOperations.transactionOrWiden(z[Connection, DbException], commitOnFailure = true)

/* transactionOrDieR */
val _: ZIO[Environment, String, Int] =
serviceOperations.transactionOrDieR[Environment](z[Connection with Environment, String])
val _: ZIO[Environment, String, Int] =
serviceOperations.transactionOrDieR[Environment](z[Connection with Environment, String], commitOnFailure = true)

/* transactionOrDie */
val _: ZIO[Any, String, Int] =
serviceOperations.transactionOrDie(z[Connection, String])
val _: ZIO[Any, String, Int] =
serviceOperations.transactionOrDie(z[Connection, String], commitOnFailure = true)

/* auto-commit */
/* autoCommitR */
val _: ZIO[Environment, Either[DbException, String], Int] =
serviceOperations.autoCommitR[Environment](z[Connection with Environment, String])

/* autoCommit */
val _: ZIO[Any, Either[DbException, String], Int] =
serviceOperations.autoCommit(z[Connection, String])

/* autoCommitOrWidenR */
val _: ZIO[Environment, Exception, Int] =
serviceOperations.autoCommitOrWidenR[Environment](z[Connection with Environment, IllegalArgumentException])
val _: ZIO[Environment, DbException, Int] =
serviceOperations.autoCommitOrWidenR[Environment](z[Connection with Environment, DbException])

/* autoCommitOrWiden */
val _: ZIO[Any, Exception, Int] =
serviceOperations.autoCommitOrWiden(z[Connection, IllegalArgumentException])
val _: ZIO[Any, DbException, Int] =
serviceOperations.autoCommitOrWiden(z[Connection, DbException])

/* autoCommitOrDieR */
val _: ZIO[Environment, String, Int] =
serviceOperations.autoCommitOrDieR[Environment](z[Connection with Environment, String])

/* autoCommitOrDie */
val _: ZIO[Any, String, Int] =
serviceOperations.autoCommitOrDie(z[Connection, String])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object AnormIT extends ITSpec[Database, PersonQueries] {
testDataCommittedOnTransactionSuccess,
testConnectionClosedOnTransactionSuccess,
testDataRollbackedOnTransactionFailure,
testDataCommittedOnTransactionFailure,
testConnectionClosedOnTransactionFailure,
testDataCommittedOnAutoCommitSuccess,
testConnectionClosedOnAutoCommitSuccess,
Expand All @@ -48,14 +49,22 @@ object AnormIT extends ITSpec[Database, PersonQueries] {
} yield assert(connectionCount)(equalTo(1)) // only the current connection
}

private val testDataRollbackedOnTransactionFailure = testM("data rollbacked on transaction failure") {
private val testDataRollbackedOnTransactionFailure = testM("data rollbacked on transaction failure if commitOnFailure=false") {
for {
_ <- Database.transactionR[PersonQueries](PersonQueries.setup)
_ <- Database.transactionR[PersonQueries](PersonQueries.insert(buffy) &&& PersonQueries.failing).flip
persons <- Database.transactionR[PersonQueries](PersonQueries.list)
} yield assert(persons)(equalTo(Nil))
}

private val testDataCommittedOnTransactionFailure = testM("data committed on transaction failure if commitOnFailure=true") {
for {
_ <- Database.transactionR[PersonQueries](PersonQueries.setup)
_ <- Database.transactionR[PersonQueries](PersonQueries.insert(buffy) &&& PersonQueries.failing, commitOnFailure = true).flip
persons <- Database.transactionR[PersonQueries](PersonQueries.list)
} yield assert(persons)(equalTo(List(buffy)))
}

private val testConnectionClosedOnTransactionFailure = testM("connection closed on transaction failure") {
for {
_ <- Database.transactionR[PersonQueries](PersonQueries.setup)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object DoobieIT extends ITSpec[Database, PersonQueries] {
testDataCommittedOnTransactionSuccess,
testConnectionClosedOnTransactionSuccess,
testDataRollbackedOnTransactionFailure,
testDataCommittedOnTransactionFailure,
testConnectionClosedOnTransactionFailure,
testDataCommittedOnAutoCommitSuccess,
testConnectionClosedOnAutoCommitSuccess,
Expand All @@ -48,14 +49,22 @@ object DoobieIT extends ITSpec[Database, PersonQueries] {
} yield assert(connectionCount)(equalTo(1)) // only the current connection
}

private val testDataRollbackedOnTransactionFailure = testM("data rollbacked on transaction failure") {
private val testDataRollbackedOnTransactionFailure = testM("data rollbacked on transaction failure if commitOnFailure=false") {
for {
_ <- Database.transactionR[PersonQueries](PersonQueries.setup)
_ <- Database.transactionR[PersonQueries](PersonQueries.insert(buffy) &&& PersonQueries.failing).flip
persons <- Database.transactionR[PersonQueries](PersonQueries.list)
} yield assert(persons)(equalTo(Nil))
}

private val testDataCommittedOnTransactionFailure = testM("data committed on transaction failure if commitOnFailure=true") {
for {
_ <- Database.transactionR[PersonQueries](PersonQueries.setup)
_ <- Database.transactionR[PersonQueries](PersonQueries.insert(buffy) &&& PersonQueries.failing, commitOnFailure = true).flip
persons <- Database.transactionR[PersonQueries](PersonQueries.list)
} yield assert(persons)(equalTo(List(buffy)))
}

private val testConnectionClosedOnTransactionFailure = testM("connection closed on transaction failure") {
for {
_ <- Database.transactionR[PersonQueries](PersonQueries.setup)
Expand Down