Last active
November 11, 2020 14:50
-
-
Save loicknuchel/2297d612b58b399395bdd08d3c6dd217 to your computer and use it in GitHub Desktop.
SQL DSL: use shapeless to require correct Put instances
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cats.effect.{ContextShift, IO} | |
import doobie.syntax.connectionio._ | |
import doobie.syntax.string._ | |
import doobie.util.Put | |
import doobie.util.fragment.Fragment | |
import doobie.util.fragment.Fragment.const0 | |
import doobie.util.transactor.Transactor | |
import org.scalatest.BeforeAndAfterEach | |
import org.scalatest.funspec.AnyFunSpec | |
import org.scalatest.matchers.should.Matchers | |
import scala.concurrent.ExecutionContext | |
/** | |
* I'm creating a jOOQ inspired DSL on top of Doobie. Ideally, I would prefer to decouple it from Doobie but it's not a requirement. | |
* | |
* I have some trouble with the INSERT INTO method that require `Put` instances of parameters but it should not require Put instances of Option[A], just A | |
* An idea can be to use shapeless to require specific types (based on list of fields) instead of any types with a Put instance. | |
* This will improve type safety as well. But I'm not very fluent in shapeless, any help is welcome ;) | |
* | |
* More generally, if you have some tips managing optionallity in generics it would be nice, I'm having some hard time with it ^^ | |
* | |
* If you want to look at the lib code: https://github.com/loicknuchel/SafeQL/blob/master/src/main/scala/fr/loicknuchel/safeql/Query.scala#L50 | |
* And where it's used: https://github.com/gospeak-io/gospeak/blob/master/infra/src/main/scala/gospeak/infra/services/storage/sql/CfpRepoSql.scala#L67 | |
*/ | |
class DemoSpec extends AnyFunSpec with Matchers with BeforeAndAfterEach { | |
private implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) | |
protected val xa: doobie.Transactor[IO] = Transactor.fromDriverManager[IO]( | |
driver = "org.h2.Driver", | |
url = "jdbc:h2:mem:my_test;MODE=PostgreSQL;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1", | |
user = "", | |
pass = "") | |
override def beforeEach(): Unit = sql"CREATE TABLE users (id INT NOT NULL, name VARCHAR(50))".update.run.transact(xa).unsafeRunSync() | |
override def afterEach(): Unit = sql"DROP TABLE users".update.run.transact(xa).unsafeRunSync() | |
/* | |
* Basic SQL DSL | |
*/ | |
case class Field[A](name: String, typ: String) | |
case class Table(name: String, fields: List[Field[_]]) { | |
def create: Fragment = const0(s"CREATE TABLE $name (${fields.map(f => f.name + " " + f.typ).mkString(", ")})") | |
def drop: Fragment = const0(s"DROP TABLE $name") | |
// FIXME: B is Option[String], but Doobie do not build Put[Option[A]] it expects a Put[A] instead and manage the Option in the quasiquotes | |
def insert[A: Put, B: Put](a: A, b: B): Fragment = const0(s"INSERT INTO $name (${fields.map(_.name).mkString(", ")}) VALUES ") ++ fr0"($a, $b)" | |
} | |
val users: Table = Table("users", List( | |
Field[Int]("id", "INT NOT NULL"), | |
Field[Option[String]]("name", "VARCHAR(50)"))) | |
it("should perform operations using the DSL") { | |
implicit def optPut[A: Put]: Put[Option[A]] = implicitly[Put[A]].contramap[Option[A]](_.get) // FIXME bad workaround that fail on None :( | |
users.insert(1, Option("Toto")).update.run.transact(xa).unsafeRunSync() shouldBe 1 | |
users.insert(2, Option.empty[String]).update.run.transact(xa).unsafeRunSync() shouldBe 1 // fails with None.get (from the optPut implicit) | |
} | |
/* | |
* Testing examples | |
*/ | |
it("should insert a user") { | |
def insertUser(id: Int, name: Option[String]): doobie.Update0 = sql"INSERT INTO users (id, name) VALUES ($id, $name)".update | |
insertUser(1, Option("Toto")).run.transact(xa).unsafeRunSync() shouldBe 1 | |
insertUser(2, None).run.transact(xa).unsafeRunSync() shouldBe 1 | |
} | |
it("should insert a user 2") { | |
def insertUser[A: Put, B: Put](id: A, name: Option[B]): doobie.Update0 = sql"INSERT INTO users (id, name) VALUES ($id, $name)".update | |
insertUser(1, Option("Toto")).run.transact(xa).unsafeRunSync() shouldBe 1 | |
insertUser(2, Option.empty[String]).run.transact(xa).unsafeRunSync() shouldBe 1 | |
} | |
it("should insert a user 3") { | |
def insertUser[A: Put, B: Put](id: A, name: B): doobie.Update0 = sql"INSERT INTO users (id, name) VALUES ($id, $name)".update | |
implicit def optPut[A: Put]: Put[Option[A]] = implicitly[Put[A]].contramap[Option[A]](_.get) // FIXME bad workaround that fail on None :( | |
insertUser(1, Option("Toto")).run.transact(xa).unsafeRunSync() shouldBe 1 | |
insertUser(2, Option.empty[String]).run.transact(xa).unsafeRunSync() shouldBe 1 // fails with None.get (from the optPut implicit) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
One solution given by @ScalaWilliam: use
Write
instead ofPut
, so Doobie is able to manageOption
:D