Skip to content

Commit

Permalink
Add ZIO module providing connect and transact mechanism adapted t…
Browse files Browse the repository at this point in the history
…o ZIO (#47)
  • Loading branch information
guizmaii authored Dec 5, 2024
1 parent a2334fd commit 835861c
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 1 deletion.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Yet another database client for Scala. No dependencies, high productivity.
* [Splicing Literal Values into Frags](#splicing-literal-values-into-frags)
* [Postgres Module](#postgres-module)
* [Logging](#logging-sql-queries)
* [Integrations](#integrations)
* [ZIO](#zio)
* [Motivation](#motivation)
* [Feature List And Database Support](#feature-list)
* [Talks and Blogs](#talks-and-blogs)
Expand Down Expand Up @@ -538,6 +540,23 @@ Setting to TRACE will log SQL queries and their parameters.

You can log slow queries by using the `Transactor` class in conjunction with `SqlLogger.logSlowQueries(FiniteDuration)`. See [Customizing Transactions](#customizing-transactions) for an example. You can also implement your own SqlLogger subclass as desired.

## Integrations

### ZIO

Magnum provides a fine layer of integration with ZIO.
The `magnum-zio` module provides an implementation of the `connect` and `transact` utils that return a ZIO effect.

To use the ZIO integration, add the following dependency:
```scala
"com.augustnagro" %% "magnum-zio" % "x.x.x"
```

and import these utils in your code with:
```scala
import com.augustnagro.magnum.magzio.*
```

## Motivation

Historically, database clients on the JVM fall into three categories.
Expand Down
13 changes: 12 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ val circeVersion = "0.14.10"

lazy val root = project
.in(file("."))
.aggregate(magnum, magnumPg)
.aggregate(magnum, magnumPg, magnumZio)

lazy val magnum = project
.in(file("magnum"))
Expand Down Expand Up @@ -82,3 +82,14 @@ lazy val magnumPg = project
"org.scala-lang.modules" %% "scala-xml" % "2.3.0" % Test
)
)

lazy val magnumZio = project
.in(file("magnum-zio"))
.dependsOn(magnum % "compile->compile;test->test")
.settings(
Test / fork := true,
publish / skip := false,
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % "2.1.12" % Provided
)
)
128 changes: 128 additions & 0 deletions magnum-zio/src/main/scala/com/augustnagro/magnum/magzio/zio.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.augustnagro.magnum.magzio

import zio.{Trace, ZIO}
import com.augustnagro.magnum.*

import java.sql.Connection
import javax.sql.DataSource
import scala.util.control.NonFatal

/** Executes a given query on a given DataSource
*
* Re-implementation for ZIO of
* [[com.augustnagro.magnum.connect(dataSource: DataSource)]]
*
* Usage:
* {{{
* import com.augustnagro.magnum.magzio.*
*
* connect(datasource) { cn ?=> repo.findById(id) }
* }}}
*/
def connect[A](dataSource: DataSource)(q: DbCon ?=> A)(implicit
trace: Trace
): ZIO[Any, Throwable, A] =
connect(Transactor(dataSource))(q)

/** Executes a given query on a given Transactor
*
* Re-implementation for ZIO of
* [[com.augustnagro.magnum.connect(dataSource: DataSource)]]
*
* Usage:
* {{{
* import com.augustnagro.magnum.magzio.*
*
* connect(transactor) { cn ?=> repo.findById(id) }
* }}}
*/
def connect[A](
transactor: Transactor
)(q: DbCon ?=> A)(implicit trace: Trace): ZIO[Any, Throwable, A] =
ZIO.blocking {
ZIO.acquireReleaseWith(
acquire = ZIO.attempt(transactor.dataSource.getConnection())
)(release =
conn => if (conn ne null) ZIO.attempt(conn.close()).orDie else ZIO.unit
) { cn =>
ZIO.attempt {
transactor.connectionConfig(cn)
q(using DbCon(cn, transactor.sqlLogger))
}
}
}

/** Executes a given transaction on a given DataSource
*
* Re-implementation for ZIO of
* [[com.augustnagro.magnum.transact(transactor: Transactor)]]
*
* Usage:
* {{{
* import com.augustnagro.magnum.magzio.*
*
* transact(dataSource) { tx ?=> repo.insertReturning(creator) }
* }}}
*/
def transact[A](dataSource: DataSource)(q: DbTx ?=> A)(implicit
trace: Trace
): ZIO[Any, Throwable, A] =
transact(Transactor(dataSource))(q)

/** Executes a given transaction on a given DataSource
*
* Re-implementation for ZIO of
* [[com.augustnagro.magnum.transact(transactor: Transactor, connectionConfig: Connection => Unit)]]
*
* Usage:
* {{{
* import com.augustnagro.magnum.magzio.*
*
* transact(dataSource, ...) { tx ?=> repo.insertReturning(creator) }
* }}}
*/
def transact[A](dataSource: DataSource, connectionConfig: Connection => Unit)(
q: DbTx ?=> A
)(implicit trace: Trace): ZIO[Any, Throwable, A] =
val transactor =
Transactor(dataSource = dataSource, connectionConfig = connectionConfig)
transact(transactor)(q)

/** Executes a given transaction on a given Transactor
*
* Re-implementation for ZIO of
* [[com.augustnagro.magnum.transact(transactor: Transactor)]]
*
* Usage:
* {{{
* import com.augustnagro.magnum.magzio.*
*
* transact(transactor) { tx ?=> repo.insertReturning(creator) }
* }}}
*/
def transact[A](
transactor: Transactor
)(q: DbTx ?=> A)(implicit trace: Trace): ZIO[Any, Throwable, A] =
ZIO.blocking {
ZIO.acquireReleaseWith(
acquire = ZIO.attempt(transactor.dataSource.getConnection())
)(release =
conn => if (conn ne null) ZIO.attempt(conn.close()).orDie else ZIO.unit
) { cn =>
ZIO.uninterruptible {
ZIO.attempt {
transactor.connectionConfig(cn)
cn.setAutoCommit(false)
try {
val res = q(using DbTx(cn, transactor.sqlLogger))
cn.commit()
res
} catch {
case NonFatal(t) =>
cn.rollback()
throw t
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package com.augustnagro.magnum.magzio

import com.augustnagro.magnum.*
import munit.FunSuite
import shared.Color
import zio.*

import java.sql.Connection
import java.time.OffsetDateTime
import scala.util.{Success, Using}

def immutableRepoZioTests(
suite: FunSuite,
dbType: DbType,
xa: () => Transactor
)(using
munit.Location,
DbCodec[OffsetDateTime]
): Unit =
import suite.*

val runtime: Runtime[Any] = zio.Runtime.default

def runIO[A](io: ZIO[Any, Throwable, A]): A =
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(io).getOrThrow()
}

@Table(dbType, SqlNameMapper.CamelToSnakeCase)
case class Car(
model: String,
@Id id: Long,
topSpeed: Int,
@SqlName("vin") vinNumber: Option[Int],
color: Color,
created: OffsetDateTime
) derives DbCodec

val carRepo = ImmutableRepo[Car, Long]
val car = TableInfo[Car, Car, Long]

val allCars = Vector(
Car(
model = "McLaren Senna",
id = 1L,
topSpeed = 208,
vinNumber = Some(123),
color = Color.Red,
created = OffsetDateTime.parse("2024-11-24T22:17:30.000000000Z")
),
Car(
model = "Ferrari F8 Tributo",
id = 2L,
topSpeed = 212,
vinNumber = Some(124),
color = Color.Green,
created = OffsetDateTime.parse("2024-11-24T22:17:31.000000000Z")
),
Car(
model = "Aston Martin Superleggera",
id = 3L,
topSpeed = 211,
vinNumber = None,
color = Color.Blue,
created = OffsetDateTime.parse("2024-11-24T22:17:32.000000000Z")
)
)

test("count"):
val count =
runIO:
magzio.connect(xa()):
carRepo.count
assert(count == 3L)

test("existsById"):
val (exists3, exists4) =
runIO:
magzio.connect(xa()):
carRepo.existsById(3L) -> carRepo.existsById(4L)
assert(exists3)
assert(!exists4)

test("findAll"):
val cars =
runIO:
magzio.connect(xa()):
carRepo.findAll
assert(cars == allCars)

test("findById"):
val (exists3, exists4) =
runIO:
magzio.connect(xa()):
carRepo.findById(3L) -> carRepo.findById(4L)
assert(exists3.get == allCars.last)
assert(exists4 == None)

test("findAllByIds"):
assume(dbType != ClickhouseDbType)
assume(dbType != MySqlDbType)
assume(dbType != OracleDbType)
assume(dbType != SqliteDbType)
val ids =
runIO:
magzio.connect(xa()):
carRepo.findAllById(Vector(1L, 3L)).map(_.id)
assert(ids == Vector(1L, 3L))

test("serializable transaction"):
val count =
runIO:
magzio.transact(xa().copy(connectionConfig = withSerializable)):
carRepo.count
assert(count == 3L)

def withSerializable(con: Connection): Unit =
con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)

test("select query"):
val minSpeed: Int = 210
val query =
sql"select ${car.all} from $car where ${car.topSpeed} > $minSpeed"
.query[Car]
val result =
runIO:
magzio.connect(xa()):
query.run()
assertNoDiff(
query.frag.sqlString,
"select model, id, top_speed, vin, color, created from car where top_speed > ?"
)
assert(query.frag.params == Vector(minSpeed))
assert(result == allCars.tail)

test("select query with aliasing"):
val minSpeed = 210
val cAlias = car.alias("c")
val query =
sql"select ${cAlias.all} from $cAlias where ${cAlias.topSpeed} > $minSpeed"
.query[Car]
val result =
runIO:
magzio.connect(xa()):
query.run()
assertNoDiff(
query.frag.sqlString,
"select c.model, c.id, c.top_speed, c.vin, c.color, c.created from car c where c.top_speed > ?"
)
assert(query.frag.params == Vector(minSpeed))
assert(result == allCars.tail)

test("select via option"):
val vin = Some(124)
val cars =
runIO:
magzio.connect(xa()):
sql"select * from car where vin = $vin"
.query[Car]
.run()
assert(cars == allCars.filter(_.vinNumber == vin))

test("tuple select"):
val tuples =
runIO:
magzio.connect(xa()):
sql"select model, color from car where id = 2"
.query[(String, Color)]
.run()
assert(tuples == Vector(allCars(1).model -> allCars(1).color))

test("reads null int as None and not Some(0)"):
val maybeCar =
runIO:
magzio.connect(xa()):
carRepo.findById(3L)
assert(maybeCar.get.vinNumber == None)

test("created timestamps should match"):
val allCars =
runIO:
magzio.connect(xa()):
carRepo.findAll
assert(allCars.map(_.created) == allCars.map(_.created))

test(".query iterator"):
val carsCount =
runIO:
magzio.connect(xa()):
Using.Manager(implicit use =>
val it = sql"SELECT * FROM car".query[Car].iterator()
it.map(_.id).size
)
assert(carsCount == Success(3))

end immutableRepoZioTests
Loading

0 comments on commit 835861c

Please sign in to comment.