Skip to content
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
11 changes: 7 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ val commonSettings = Seq(
scalacOptions in (Test, compile) ~= (_.filterNot(
Set(
"-Ywarn-unused:imports",
"-Xfatal-warnings"
"-Xfatal-warnings",
"-Yrangepos"
))),
resolvers ++= Seq[Resolver](
Resolver.sonatypeRepo("releases")
Expand All @@ -60,7 +61,10 @@ lazy val core = (project in file("core"))
.settings(
name := "query-core",
libraryDependencies ++= Seq(
Dependencies.cats
Dependencies.acolyte % Test,
Dependencies.anorm % Test,
Dependencies.cats,
Dependencies.specs2 % Test
)
)

Expand All @@ -72,8 +76,7 @@ lazy val sampleAppExample = (project in file("examples/sample-app"))
libraryDependencies ++= Seq(
jdbc,
Dependencies.anorm,
Dependencies.h2,
Dependencies.scalaTestPlusPlay
Dependencies.h2
)
)
.dependsOn(core)
Expand Down
48 changes: 48 additions & 0 deletions core/src/main/scala/core/database/ComposeWithCompletion.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.zengularity.querymonad.core.database

import scala.concurrent.{ExecutionContext, Future}
import scala.language.higherKinds

/**
* Heavily inspired from work done by @cchantep in Acolyte (see acolyte.reactivemongo.ComposeWithCompletion)
*/
trait ComposeWithCompletion[F[_], Out] {
type Outer <: Future[_]

def apply[In](resource: In, f: In => F[Out])(onComplete: In => Unit)(
implicit ec: ExecutionContext): Outer
}

object ComposeWithCompletion extends LowPriorityCompose {

type Aux[F[_], A, B] = ComposeWithCompletion[F, A] { type Outer = Future[B] }

implicit def futureOut[A]: Aux[Future, A, A] =
new ComposeWithCompletion[Future, A] {
type Outer = Future[A]

def apply[In](resource: In, f: In => Future[A])(onComplete: In => Unit)(
implicit ec: ExecutionContext): Outer =
f(resource).andThen {
case _ => onComplete(resource)
}

override val toString = "futureOut"
}

}

trait LowPriorityCompose { _: ComposeWithCompletion.type =>

implicit def pureOut[F[_], A]: Aux[F, A, F[A]] =
new ComposeWithCompletion[F, A] {
type Outer = Future[F[A]]

def apply[In](resource: In, f: In => F[A])(onComplete: In => Unit)(
implicit ec: ExecutionContext): Outer =
Future(f(resource)).andThen { case _ => onComplete(resource) }

override val toString = "pureOut"
}

}
37 changes: 28 additions & 9 deletions core/src/main/scala/core/database/QueryRunner.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
package com.zengularity.querymonad.core.database

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext
import scala.language.higherKinds

/**
* A class who can run a Query.
*/
sealed trait QueryRunner[Resource] {
def apply[T](query: Query[Resource, T]): Future[T]
def apply[M[_], T](
query: QueryT[M, Resource, T]
)(
implicit compose: ComposeWithCompletion[M, T]
): compose.Outer
}

object QueryRunner {
private class DefaultRunner[Resource](wr: WithResource[Resource])(
implicit ec: ExecutionContext)
extends QueryRunner[Resource] {
def apply[T](query: Query[Resource, T]): Future[T] =
Future(wr(query.run))
private class DefaultRunner[Resource](
wr: WithResource[Resource]
)(
implicit ec: ExecutionContext
) extends QueryRunner[Resource] {

def apply[M[_], T](
query: QueryT[M, Resource, T]
)(
implicit compose: ComposeWithCompletion[M, T]
): compose.Outer = {
wr { resource =>
compose(resource, query.run)(wr.releaseIfNecessary)
}
}

}

// Default factory
def apply[Resource](wr: WithResource[Resource])(
implicit ec: ExecutionContext): QueryRunner[Resource] =
def apply[Resource](
wr: WithResource[Resource]
)(
implicit ec: ExecutionContext
): QueryRunner[Resource] =
new DefaultRunner(wr)
}
2 changes: 2 additions & 0 deletions core/src/main/scala/core/database/WithResource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ package com.zengularity.querymonad.core.database

trait WithResource[Resource] {
def apply[A](f: Resource => A): A

def releaseIfNecessary(resource: Resource): Unit
}
22 changes: 18 additions & 4 deletions core/src/main/scala/core/database/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.zengularity.querymonad.core

import scala.language.higherKinds

import cats.{Applicative, Id}
import cats.Applicative

import cats.Id
import cats.data.{Reader, ReaderT}

package object database {
Expand All @@ -19,16 +21,28 @@ package object database {
type QueryT[F[_], Resource, A] = ReaderT[F, Resource, A]

object QueryT {
def apply[M[_], Resource, A](
run: Resource => M[A]
): QueryT[M, Resource, A] =
new QueryT(run)

def pure[M[_]: Applicative, Resource, A](a: A) =
ReaderT.pure[M, Resource, A](a)

def ask[M[_]: Applicative, Resource] = ReaderT.ask[M, Resource]
def ask[M[_]: Applicative, Resource] =
ReaderT.ask[M, Resource]

def liftF[M[_], Resource, A](ma: M[A]) =
ReaderT.liftF[M, Resource, A](ma)

def liftF[M[_], Resource, A](ma: M[A]) = ReaderT.liftF[M, Resource, A](ma)
def fromQuery[M[_], Resource, A](
query: Query[Resource, M[A]]
): QueryT[M, Resource, A] =
QueryT[M, Resource, A](query.run)
}

type QueryO[Resource, A] = QueryT[Option, Resource, A]

type QueryE[Resource, A, Err] =
type QueryE[Resource, Err, A] =
QueryT[({ type F[T] = Either[Err, T] })#F, Resource, A]
}
32 changes: 32 additions & 0 deletions core/src/main/scala/core/module/sql/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ package com.zengularity.querymonad.core.module
import java.sql.Connection

import scala.concurrent.ExecutionContext
import scala.language.higherKinds

import cats.Applicative

import com.zengularity.querymonad.core.database.{
Query,
QueryRunner,
QueryT,
QueryO,
QueryE,
WithResource
}

package object sql {

// Query aliases
type SqlQuery[A] = Query[Connection, A]

object SqlQuery {
Expand All @@ -21,6 +29,29 @@ package object sql {
def apply[A](f: Connection => A) = new SqlQuery(f)
}

// Query transformer aliases
type SqlQueryT[F[_], A] = QueryT[F, Connection, A]

object SqlQueryT {
def apply[M[_], A](run: Connection => M[A]) =
QueryT.apply[M, Connection, A](run)

def pure[M[_]: Applicative, A](a: A) =
QueryT.pure[M, Connection, A](a)

def ask[M[_]: Applicative] = QueryT.ask[M, Connection]

def liftF[M[_], A](ma: M[A]) = QueryT.liftF[M, Connection, A](ma)

def fromQuery[M[_], A](query: SqlQuery[M[A]]) =
QueryT.fromQuery[M, Connection, A](query)
}

type SqlQueryO[A] = QueryO[Connection, A]

type SqlQueryE[A, Err] = QueryE[Connection, A, Err]

// Query runner aliases
type WithSqlConnection = WithResource[Connection]

type SqlQueryRunner = QueryRunner[Connection]
Expand All @@ -30,4 +61,5 @@ package object sql {
implicit ec: ExecutionContext): SqlQueryRunner =
QueryRunner[Connection](wc)
}

}
148 changes: 148 additions & 0 deletions core/src/test/scala/module/sql/SqlQueryRunnerSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.zengularity.querymonad.test.core.module.sql

import acolyte.jdbc.{
AcolyteDSL,
ExecutedParameter,
QueryExecution,
QueryResult => AcolyteQueryResult
}
import org.specs2.concurrent.ExecutionEnv
import org.specs2.mutable.Specification

import com.zengularity.querymonad.core.module.sql.{
SqlQuery,
SqlQueryT,
SqlQueryRunner,
WithSqlConnection
}
import com.zengularity.querymonad.test.core.module.sql.models.{
Material,
Professor
}
import com.zengularity.querymonad.test.core.module.sql.utils.SqlConnectionFactory

class SqlQueryRunnerSpec(implicit ee: ExecutionEnv) extends Specification {

"SqlQueryRunner" should {
// execute lift Queries
"return integer value lift in Query using pure" in {
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
val runner = SqlQueryRunner(withSqlConnection)
val query = SqlQuery.pure(1)

runner(query) aka "material" must beTypedEqualTo(1).await
}

"return optional value lift in Query using liftF" in {
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
val runner = SqlQueryRunner(withSqlConnection)
val query = SqlQueryT.liftF(Seq(1))

runner(query) aka "material" must beTypedEqualTo(Seq(1)).await
}

// execute single query
"retrieve professor with id 1" in {
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(Professor.resultSet)
val runner = SqlQueryRunner(withSqlConnection)
val result = runner(Professor.fetchProfessor(1)).map(_.get)

result aka "professor" must beTypedEqualTo(
Professor(1, "John Doe", 35, 1)).await
}

"retrieve material with id 1" in {
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(Material.resultSet)
val runner = SqlQueryRunner(withSqlConnection)
val result = runner(Material.fetchMaterial(1)).map(_.get)

result aka "material" must beTypedEqualTo(
Material(1, "Computer Science", 20, "Beginner")).await
}

"not retrieve professor with id 2" in {
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(AcolyteQueryResult.Nil)
val runner = SqlQueryRunner(withSqlConnection)
val query = for {
_ <- SqlQuery.ask
professor <- Professor.fetchProfessor(2)
} yield professor

runner(query) aka "material" must beNone.await
}

// execute composed queries into a single transaction
"retrieve professor with id 1 and his material" in {
val handler = AcolyteDSL.handleQuery {
case QueryExecution("SELECT * FROM professors where id = ?",
ExecutedParameter(1) :: Nil) =>
Professor.resultSet
case QueryExecution("SELECT * FROM materials where id = ?",
ExecutedParameter(1) :: Nil) =>
Material.resultSet
case _ =>
AcolyteQueryResult.Nil
}
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(handler)
val runner = SqlQueryRunner(withSqlConnection)
val query = for {
professor <- Professor.fetchProfessor(1).map(_.get)
material <- Material.fetchMaterial(professor.material).map(_.get)
} yield (professor, material)

runner(query) aka "professor and material" must beTypedEqualTo(
Tuple2(Professor(1, "John Doe", 35, 1),
Material(1, "Computer Science", 20, "Beginner"))).await
}

"not retrieve professor with id 1 and no material" in {
import cats.instances.option._
val handler = AcolyteDSL.handleQuery {
case QueryExecution("SELECT * FROM professors where id = {id}", _) =>
Professor.resultSet
case _ =>
AcolyteQueryResult.Nil
}
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(handler)
val runner = SqlQueryRunner(withSqlConnection)
val query = for {
professor <- SqlQueryT.fromQuery(Professor.fetchProfessor(1))
material <- SqlQueryT.fromQuery(
Material.fetchMaterial(professor.material))
} yield (professor, material)

runner(query) aka "professor and material" must beNone.await
}

// execute async queries
"retrieve int value fetch in an async context" in {
import scala.concurrent.Future
import anorm.{SQL, SqlParser}
import acolyte.jdbc.RowLists
import acolyte.jdbc.Implicits._
val queryResult: AcolyteQueryResult =
(RowLists.rowList1(classOf[Int] -> "res").append(5))
val withSqlConnection: WithSqlConnection =
SqlConnectionFactory.withSqlConnection(queryResult)
val runner = SqlQueryRunner(withSqlConnection)
val query =
SqlQueryT { implicit connection =>
Future {
Thread.sleep(900) // to simulate a slow-down
SQL("SELECT 5 as res")
.as(SqlParser.int("res").single)
}
}

runner(query) aka "result" must beTypedEqualTo(5).await
}
}

}
Loading