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

Add ZIO module providing connect and transact mechanisms adapted to ZIO #47

Merged
merged 1 commit into from
Dec 5, 2024
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
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
Loading