Skip to content

Commit 61e9f14

Browse files
Adds support for cats effects IO
1 parent 3bd0def commit 61e9f14

File tree

7 files changed

+266
-0
lines changed

7 files changed

+266
-0
lines changed

build.sbt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ lazy val core = (project in file("core"))
7878
)
7979
)
8080

81+
lazy val catsEffectsIOModule = (project in file("modules/cats-effect-io"))
82+
.settings(commonSettings)
83+
.settings(
84+
name := "query-cats-effect-io",
85+
libraryDependencies ++= Seq(
86+
Dependencies.catsEffect,
87+
)
88+
)
89+
.dependsOn(core % "test->test;compile->compile")
90+
8191
lazy val playSqlModule = (project in file("modules/play-sql"))
8292
.settings(commonSettings)
8393
.settings(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.zengularity.querymonad.module.catsio
2+
3+
import scala.language.higherKinds
4+
5+
import cats.effect.IO
6+
7+
import com.zengularity.querymonad.core.database.LiftAsync
8+
9+
trait LiftAsyncIO extends LowPriority {
10+
11+
implicit def ioOut[A]: LiftAsync.Aux[IO, IO, A, A] =
12+
new LiftAsync[IO, IO, A] {
13+
type Outer = A
14+
15+
def apply[In](
16+
loaner: WithResourceIO[In],
17+
f: In => IO[A]
18+
): IO[Outer] = loaner(f)
19+
20+
override val toString = "ioOut"
21+
}
22+
23+
}
24+
25+
trait LowPriority { _: LiftAsyncIO =>
26+
27+
implicit def pureOut[F[_], A]: LiftAsync.Aux[IO, F, A, F[A]] =
28+
new LiftAsync[IO, F, A] {
29+
type Outer = F[A]
30+
31+
def apply[In](loaner: WithResourceIO[In], f: In => F[A]): IO[Outer] =
32+
loaner(r => IO(f(r)))
33+
34+
override val toString = "pureOut"
35+
}
36+
37+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.zengularity.querymonad.module
2+
3+
import cats.effect.IO
4+
import com.zengularity.querymonad.core.database.{QueryRunner, WithResource}
5+
6+
package object catsio {
7+
type WithResourceIO[Resource] = WithResource[IO, Resource]
8+
9+
type QueryRunnerIO[Resource] = QueryRunner[IO, Resource]
10+
11+
object QueryRunnerIO {
12+
def apply[Resource](wc: WithResourceIO[Resource]): QueryRunnerIO[Resource] =
13+
QueryRunnerIO[Resource](wc)
14+
}
15+
16+
object implicits extends LiftAsyncIO
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.zengularity.querymonad.module.catsio
2+
3+
import cats.effect.IO
4+
import com.zengularity.querymonad.module.sql.{SqlQueryRunner, WithSqlConnection}
5+
6+
package object sql {
7+
8+
type WithSqlConnectionIO = WithSqlConnection[IO]
9+
10+
type SqlQueryRunnerIO = SqlQueryRunner[IO]
11+
12+
object SqlQueryRunnerIO {
13+
def apply(wc: WithSqlConnectionIO): SqlQueryRunnerIO =
14+
SqlQueryRunner(wc)
15+
}
16+
17+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package module.sql
2+
3+
import acolyte.jdbc.{
4+
AcolyteDSL,
5+
ExecutedParameter,
6+
QueryExecution,
7+
QueryResult => AcolyteQueryResult
8+
}
9+
import cats.effect.IO
10+
import com.zengularity.querymonad.module.catsio.sql.WithSqlConnectionIO
11+
import org.specs2.mutable.Specification
12+
import com.zengularity.querymonad.module.sql.{
13+
SqlQuery,
14+
SqlQueryRunner,
15+
SqlQueryT
16+
}
17+
import com.zengularity.querymonad.test.module.catsio.sql.utils.SqlConnectionFactory
18+
import com.zengularity.querymonad.module.catsio.implicits._
19+
import com.zengularity.querymonad.test.module.sql.models.{Material, Professor}
20+
21+
class SqlQueryRunnerSpec extends Specification {
22+
23+
"SqlQueryRunner" should {
24+
// execute lift Queries
25+
"return integer value lift in Query using pure" in {
26+
val withSqlConnection: WithSqlConnectionIO =
27+
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
28+
val runner = SqlQueryRunner(withSqlConnection)
29+
val query = SqlQuery.pure(1)
30+
31+
runner(query).unsafeRunSync() aka "material" must beTypedEqualTo(1)
32+
}
33+
34+
"return optional value lift in Query using liftF" in {
35+
val withSqlConnection: WithSqlConnectionIO =
36+
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
37+
val runner = SqlQueryRunner(withSqlConnection)
38+
val query = SqlQueryT.liftF(Seq(1))
39+
40+
runner(query).unsafeRunSync() aka "material" must beTypedEqualTo(Seq(1))
41+
}
42+
43+
// execute single query
44+
"retrieve professor with id 1" in {
45+
val withSqlConnection: WithSqlConnectionIO =
46+
SqlConnectionFactory.withSqlConnection(Professor.resultSet)
47+
val runner = SqlQueryRunner(withSqlConnection)
48+
val result = runner(Professor.fetchProfessor(1)).map(_.get)
49+
50+
result.unsafeRunSync() aka "professor" must beTypedEqualTo(
51+
Professor(1, "John Doe", 35, 1)
52+
)
53+
}
54+
55+
"retrieve material with id 1" in {
56+
val withSqlConnection: WithSqlConnectionIO =
57+
SqlConnectionFactory.withSqlConnection(Material.resultSet)
58+
val runner = SqlQueryRunner(withSqlConnection)
59+
val result = runner(Material.fetchMaterial(1)).map(_.get)
60+
61+
result.unsafeRunSync() aka "material" must beTypedEqualTo(
62+
Material(1, "Computer Science", 20, "Beginner")
63+
)
64+
}
65+
66+
"not retrieve professor with id 2" in {
67+
val withSqlConnection: WithSqlConnectionIO =
68+
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
69+
val runner = SqlQueryRunner(withSqlConnection)
70+
val query = for {
71+
_ <- SqlQuery.ask
72+
professor <- Professor.fetchProfessor(2)
73+
} yield professor
74+
75+
runner(query).unsafeRunSync() aka "material" must beNone
76+
}
77+
78+
// execute composed queries into a single transaction
79+
"retrieve professor with id 1 and his material" in {
80+
val handler = AcolyteDSL.handleQuery {
81+
case QueryExecution("SELECT * FROM professors where id = ?",
82+
ExecutedParameter(1) :: Nil) =>
83+
Professor.resultSet
84+
case QueryExecution("SELECT * FROM materials where id = ?",
85+
ExecutedParameter(1) :: Nil) =>
86+
Material.resultSet
87+
case _ =>
88+
AcolyteQueryResult.Nil
89+
}
90+
val withSqlConnection: WithSqlConnectionIO =
91+
SqlConnectionFactory.withSqlConnection(handler)
92+
val runner = SqlQueryRunner(withSqlConnection)
93+
val query = for {
94+
professor <- Professor.fetchProfessor(1).map(_.get)
95+
material <- Material.fetchMaterial(professor.material).map(_.get)
96+
} yield (professor, material)
97+
98+
runner(query)
99+
.unsafeRunSync() aka "professor and material" must beTypedEqualTo(
100+
Tuple2(Professor(1, "John Doe", 35, 1),
101+
Material(1, "Computer Science", 20, "Beginner"))
102+
)
103+
}
104+
105+
"not retrieve professor with id 1 and no material" in {
106+
import cats.instances.option._
107+
val handler = AcolyteDSL.handleQuery {
108+
case QueryExecution("SELECT * FROM professors where id = {id}", _) =>
109+
Professor.resultSet
110+
case _ =>
111+
AcolyteQueryResult.Nil
112+
}
113+
val withSqlConnection: WithSqlConnectionIO =
114+
SqlConnectionFactory.withSqlConnection(handler)
115+
val runner = SqlQueryRunner(withSqlConnection)
116+
val query = for {
117+
professor <- SqlQueryT.fromQuery(Professor.fetchProfessor(1))
118+
material <- SqlQueryT.fromQuery(
119+
Material.fetchMaterial(professor.material)
120+
)
121+
} yield (professor, material)
122+
123+
runner(query).unsafeRunSync() aka "professor and material" must beNone
124+
}
125+
126+
// execute async queries
127+
"retrieve int value fetch in an async context" in {
128+
import anorm.{SQL, SqlParser}
129+
import acolyte.jdbc.RowLists
130+
import acolyte.jdbc.Implicits._
131+
132+
val queryResult: AcolyteQueryResult =
133+
(RowLists.rowList1(classOf[Int] -> "res").append(5))
134+
val withSqlConnection: WithSqlConnectionIO =
135+
SqlConnectionFactory.withSqlConnection(queryResult)
136+
val runner = SqlQueryRunner(withSqlConnection)
137+
val query =
138+
SqlQueryT { implicit connection =>
139+
IO {
140+
Thread.sleep(1000) // to simulate a slow-down
141+
SQL("SELECT 5 as res")
142+
.as(SqlParser.int("res").single)
143+
}
144+
}
145+
146+
runner(query).unsafeRunSync() aka "result" must beTypedEqualTo(5)
147+
}
148+
}
149+
150+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.zengularity.querymonad.test.module.catsio.sql.utils
2+
3+
import java.sql.Connection
4+
5+
import acolyte.jdbc.{
6+
AcolyteDSL,
7+
ScalaCompositeHandler,
8+
QueryResult => AcolyteQueryResult
9+
}
10+
import cats.effect.IO
11+
import com.zengularity.querymonad.module.catsio.sql.WithSqlConnectionIO
12+
13+
object SqlConnectionFactory {
14+
15+
def withSqlConnection[A <: AcolyteQueryResult](
16+
resultsSet: A
17+
): WithSqlConnectionIO =
18+
new WithSqlConnectionIO {
19+
def apply[B](f: Connection => IO[B]): IO[B] =
20+
AcolyteDSL.withQueryResult(resultsSet) { connection =>
21+
f(connection).guarantee(IO(connection.close()))
22+
}
23+
}
24+
25+
def withSqlConnection(handler: ScalaCompositeHandler): WithSqlConnectionIO =
26+
new WithSqlConnectionIO {
27+
def apply[B](f: Connection => IO[B]): IO[B] = {
28+
val connection = AcolyteDSL.connection(handler)
29+
f(connection).guarantee(IO(connection.close()))
30+
}
31+
}
32+
33+
}

project/Dependencies.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ object Dependencies {
88
lazy val anorm = "org.playframework.anorm" %% "anorm" % "2.6.0"
99

1010
lazy val cats = "org.typelevel" %% "cats-core" % "1.0.1"
11+
12+
lazy val catsEffect = "org.typelevel" %% "cats-effect" % "1.0.0"
1113

1214
lazy val h2 = "com.h2database" % "h2" % "1.4.196"
1315

0 commit comments

Comments
 (0)