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

Introduces DbContext to allow migration to Doobie-RC4 #48

Merged
merged 5 commits into from
Aug 5, 2023
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
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ Again, the code for Anorm is identical, except it has a different import: `io.gi
import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._
import zio.clock.Clock

val dbLayer: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource
```
Expand Down Expand Up @@ -289,7 +288,7 @@ Database.transaction(???) // es is passed implicitly to the method

```scala
val es: ErrorStrategies = ErrorStrategies.retryForeverFixed(10.seconds)
val dbLayerFromDatasource: ZLayer[Has[DataSource] with Clock, Nothing, Database] = Database.fromDatasource(es)
val dbLayerFromDatasource: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource(es)
```

#### Defining an ErrorStrategies instance
Expand Down Expand Up @@ -353,20 +352,36 @@ import zio._
import doobie.implicits._
import io.github.gaelrenoux.tranzactio.DbException
import io.github.gaelrenoux.tranzactio.doobie._
import zio.clock.Clock

val liveQuery: ZIO[Connection, DbException, List[String]] = tzio { sql"SELECT name FROM users".query[String].to[List] }
val testQuery: ZIO[Connection, DbException, List[String]] = ZIO.succeed(List("Buffy Summers"))

val liveEffect: ZIO[Database, DbException, List[String]] = Database.transactionOrWiden(liveQuery)
val testEffect: ZIO[Database, DbException, List[String]] = Database.transactionOrWiden(testQuery)

val willFail: ZIO[Clock, Any, List[String]] = liveEffect.provideLayer(Database.none) // THIS WILL FAIL
val testing: ZIO[Clock, Any, List[String]] = testEffect.provideLayer(Database.none) // This will work
val willFail: ZIO[Any, DbException, List[String]] = liveEffect.provideLayer(Database.none) // THIS WILL FAIL
val testing: ZIO[Any, DbException, List[String]] = testEffect.provideLayer(Database.none) // This will work
```



### Doobie-specific stuff

#### Log handler

From Doobie RC3 and onward, the `LogHandler` is no longer passed when building the query, but when running the transaction instead.
In TranzactIO, you can define a `LogHandler` through the `DbContext`. This context is passed implicitly when creating the Database layer, so you just need to declare your own.

```scala
import doobie.util.log.LogHandler
import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._
import zio.interop.catz._

implicit val doobieContext: DbContext = DbContext(logHandler = LogHandler.jdkLogHandler[Task])
val dbLayer: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource
```


## FAQ
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.gaelrenoux.tranzactio

import io.github.gaelrenoux.tranzactio.test.DatabaseModuleTestOps
import io.github.gaelrenoux.tranzactio.test.{DatabaseModuleTestOps, NoopJdbcConnection}
import zio.ZIO.attemptBlocking
import zio.{Tag, ZIO, ZLayer, Trace}
import zio.{Tag, Trace, ZIO, ZLayer}

import java.sql.{Connection => JdbcConnection}

Expand All @@ -11,6 +11,7 @@ import java.sql.{Connection => JdbcConnection}
package object anorm extends Wrapper {
override final type Connection = JdbcConnection
override final type Database = Database.Service
override final type DbContext = EmptyDbContext.type
override final type Query[A] = JdbcConnection => A
override final type TranzactIO[A] = ZIO[Connection, DbException, A]

Expand All @@ -23,25 +24,21 @@ package object anorm extends Wrapper {

/** Database for the Anorm wrapper */
object Database
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection]]
with DatabaseModuleTestOps[Connection] {
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext]
with DatabaseModuleTestOps[Connection, DbContext] {
self =>

private[tranzactio] override implicit val connectionTag: Tag[Connection] = anorm.connectionTag

/** How to provide a Connection for the module, given a JDBC connection and some environment. */
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] = {
ZIO.succeed(connection)
}
override def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Connection] = ZIO.succeed(NoopJdbcConnection)

/** Creates a Database Layer which requires an existing ConnectionSource. */
override final def fromConnectionSource(implicit trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) =>
new DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
self.connectionFromJdbc(connection)
}
}
override final def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) => new DatabaseService(cs) }
}

private class DatabaseService(cs: ConnectionSource) extends DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
ZIO.succeed(connection)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.github.gaelrenoux.tranzactio


import zio.{Tag, ZIO, ZLayer, Trace}
import zio.{Tag, Trace, ZIO, ZLayer}

import java.sql.{Connection => JdbcConnection}
import javax.sql.DataSource

/** Template implementing the commodity methods for a Db module. */
abstract class DatabaseModuleBase[Connection, Database <: DatabaseOps.ServiceOps[Connection] : Tag]
abstract class DatabaseModuleBase[Connection, Database <: DatabaseOps.ServiceOps[Connection] : Tag, DbContext]
extends DatabaseOps.ModuleOps[Connection, Database] {

type Service = DatabaseOps.ServiceOps[Connection]
Expand All @@ -24,32 +23,29 @@ abstract class DatabaseModuleBase[Connection, Database <: DatabaseOps.ServiceOps
override def autoCommit[R, E, A](
zio: => ZIO[Connection with R, E, A]
)(implicit errorStrategies: ErrorStrategiesRef = ErrorStrategies.Parent, trace: Trace): ZIO[Database with R, Either[DbException, E], A] = {
ZIO.serviceWithZIO[Database]{ db=>
ZIO.serviceWithZIO[Database] { db =>
db.autoCommit[R, E, A](zio)
}
}

/** Creates a Database Layer which requires an existing ConnectionSource. */
def fromConnectionSource(implicit trace: Trace): ZLayer[ConnectionSource, Nothing, Database]

/** Creates a Tranzactio Connection, given a JDBC connection and a Blocking. Useful for some utilities. */
def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection]
def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, Database]

/** Commodity method: creates a Database Layer which includes its own ConnectionSource based on a DataSource. Most
* connection pool implementations should be able to provide you a DataSource.
*
* When no implicit ErrorStrategies is available, the default ErrorStrategies will be used.
*/
final def fromDatasource(implicit trace: Trace): ZLayer[DataSource, Nothing, Database] =
final def fromDatasource(implicit dbContext: DbContext, trace: Trace): ZLayer[DataSource, Nothing, Database] =
ConnectionSource.fromDatasource >>> fromConnectionSource

/** As `fromDatasource`, but provides a default ErrorStrategiesRef. When a method is called with no available implicit
* ErrorStrategiesRef, the ErrorStrategiesRef in argument will be used. */
final def fromDatasource(errorStrategies: ErrorStrategiesRef)(implicit trace: Trace): ZLayer[DataSource, Nothing, Database] =
final def fromDatasource(errorStrategies: ErrorStrategiesRef)(implicit dbContext: DbContext, trace: Trace): ZLayer[DataSource, Nothing, Database] =
ConnectionSource.fromDatasource(errorStrategies) >>> fromConnectionSource

/** As `fromDatasource(ErrorStrategiesRef)`, but an `ErrorStrategies` is provided through a layer instead of as a parameter. */
final def fromDatasourceAndErrorStrategies(implicit trace: Trace): ZLayer[DataSource with ErrorStrategies, Nothing, Database] =
final def fromDatasourceAndErrorStrategies(implicit dbContext: DbContext, trace: Trace): ZLayer[DataSource with ErrorStrategies, Nothing, Database] =
ConnectionSource.fromDatasourceAndErrorStrategies >>> fromConnectionSource

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.gaelrenoux.tranzactio

/** To be used by libraries where a DbContext is not necessary */
object EmptyDbContext {
implicit val Default: EmptyDbContext.type = EmptyDbContext
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ trait Wrapper {

val Database: DatabaseOps.ModuleOps[Connection, _ <: DatabaseOps.ServiceOps[Connection]] // scalastyle:ignore field.name

/** Contextual information for the DB, its content depending on the underlying library. */
type DbContext

/** The specific type used in the wrapped library to represent an SQL query. */
type Query[A]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import io.github.gaelrenoux.tranzactio._
import zio.{Tag, ZEnvironment, ZIO, ZLayer, Trace}

/** Testing utilities on the Database module. */
trait DatabaseModuleTestOps[Connection] extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection]] {
trait DatabaseModuleTestOps[Connection, DbContext] extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext] {

type AnyDatabase = DatabaseOps.ServiceOps[Connection]

private[tranzactio] implicit val connectionTag: Tag[Connection]

/** A Connection which is incapable of running anything, to use when unit testing (and the queries are actually stubbed,
* so they do not need a Database). Trying to run actual queries against it will fail. */
def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Connection] = connectionFromJdbc(NoopJdbcConnection)
def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Connection]

/** A Database which is incapable of running anything, to use when unit testing (and the queries are actually stubbed,
* so they do not need a Database). Trying to run actual queries against it will fail. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package io.github.gaelrenoux.tranzactio

import _root_.doobie.free.KleisliInterpreter
import _root_.doobie.LogHandler
import _root_.doobie.util.transactor.{Strategy, Transactor}
import cats.effect.Resource
import io.github.gaelrenoux.tranzactio.test.DatabaseModuleTestOps
import io.github.gaelrenoux.tranzactio.test.NoopJdbcConnection
import zio.interop.catz._
import zio.stream.ZStream
import zio.stream.interop.fs2z._
import zio.{Tag, Task, ZIO, ZLayer, Trace}
import zio.{Tag, Task, Trace, ZIO, ZLayer}

import java.sql.{Connection => JdbcConnection}

Expand All @@ -22,7 +24,17 @@ package object doobie extends Wrapper {
private[tranzactio] val connectionTag = implicitly[Tag[Connection]]

/** Default queue size when converting from FS2 streams. Same default value as in FS2RIOStreamSyntax.toZStream. */
final val DefaultStreamQueueSize = 16
private final val DefaultStreamQueueSize = 16

/** How to provide a Connection for the module, given a JDBC connection and some environment. */
private final def transactorFromJdbcConnection(connection: => JdbcConnection, dbContext: DbContext)(implicit trace: Trace): ZIO[Any, Nothing, Connection] = {
ZIO.succeed {
val connect = (c: JdbcConnection) => Resource.pure[Task, JdbcConnection](c)
val interp = KleisliInterpreter[Task](dbContext.logHandler).ConnectionInterpreter
val tran = Transactor(connection, connect, interp, Strategy.void)
tran
}
}

override final def tzio[A](q: => Query[A])(implicit trace: Trace): TranzactIO[A] =
ZIO.serviceWithZIO[Connection] { c =>
Expand All @@ -37,30 +49,31 @@ package object doobie extends Wrapper {

/** Database for the Doobie wrapper */
object Database
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection]]
with DatabaseModuleTestOps[Connection] {
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext]
with DatabaseModuleTestOps[Connection, DbContext] {
self =>

private[tranzactio] override implicit val connectionTag: Tag[Connection] = doobie.connectionTag

/** How to provide a Connection for the module, given a JDBC connection and some environment. */
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] = {
ZIO.succeed {
val connect = (c: JdbcConnection) => Resource.pure[Task, JdbcConnection](c)
val interp = KleisliInterpreter[Task].ConnectionInterpreter
val tran = Transactor(connection, connect, interp, Strategy.void)
tran
}
}
override def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Transactor[Task]] =
transactorFromJdbcConnection(NoopJdbcConnection, DbContext.Default)

/** Creates a Database Layer which requires an existing ConnectionSource. */
override final def fromConnectionSource(implicit trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) =>
new DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
self.connectionFromJdbc(connection)
}
}
override final def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) => new DatabaseService(cs, dbContext) }
}

private class DatabaseService(cs: ConnectionSource, val dbContext: DbContext) extends DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Transactor[Task]] =
transactorFromJdbcConnection(connection, dbContext)
}

case class DbContext(
logHandler: LogHandler[Task]
)

object DbContext {
implicit val Default: DbContext = DbContext(LogHandler.noop[Task])
}

}
1 change: 0 additions & 1 deletion examples/src/test/scala/samples/anorm/LayeredApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.github.gaelrenoux.tranzactio.anorm._
import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategiesRef}
import samples.{Conf, ConnectionPool, Person}
import zio._
import zio.Console

/** A sample app where all modules are linked through ZLayer. Should run as is (make sure you have com.h2database:h2 in
* your dependencies). */
Expand Down
5 changes: 4 additions & 1 deletion examples/src/test/scala/samples/doobie/LayeredApp.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package samples.doobie

import doobie.util.log.LogHandler
import io.github.gaelrenoux.tranzactio.doobie._
import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategiesRef}
import samples.{Conf, ConnectionPool, Person}
import zio._
import zio.Console
import zio.interop.catz._

/** A sample app where all modules are linked through ZLayer. Should run as is (make sure you have com.h2database:h2 in
* your dependencies). */
Expand All @@ -13,6 +14,8 @@ object LayeredApp extends zio.ZIOAppDefault {
private val conf = Conf.live("samble-doobie-app")
private val dbRecoveryConf = conf >>> ZLayer.fromFunction((_: Conf).dbRecovery)
private val datasource = conf >>> ConnectionPool.live
// The DbContext is optional, default is to have the noop LogHandler
implicit val dbContext: DbContext = DbContext(logHandler = LogHandler.jdkLogHandler[Task])
private val database = (datasource ++ dbRecoveryConf) >>> Database.fromDatasourceAndErrorStrategies
private val personQueries = PersonQueries.live

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategiesRef}
import samples.{Conf, ConnectionPool, Person}
import zio._
import zio.stream._
import zio.Console

/** Same as LayeredApp, but using Doobie's stream (converted into ZIO strem). */
// scalastyle:off magic.number
Expand Down
2 changes: 1 addition & 1 deletion project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ object BuildHelper {
val zio = "2.0.15"
val zioCats = "23.0.0.8"
val cats = "2.9.0"
val doobie = "1.0.0-RC2"
val doobie = "1.0.0-RC4"
val anorm = "2.7.0"
val h2 = "2.2.220"
val scala212 = "2.12.18"
Expand Down