Search code examples
scalatransactionsscalatestcats-effectskunk

How to rollback a skunk transaction for integration tests? (missing implicit Origin)


I'm writing integration tests of skunk code against postgres using scala-test-containers, munit and munit-cats-effect, using Mill and Scala 3. I'm new to Cats Effect.

The setup I'm working to achieve is:

  • The test suite starts the postgres container and creates the skunk session once for the entire suite (and stops the container when the suite is finished).
  • Each test occurs within its own skunk database transaction that rolls back, to keep tests isolated.

Below is the code so far, which the compiler is ok with except for the call to rollback, which requires an implicit skunk.util.Origin. I don't see any reference to this type in the skunk docs and I can't figure out how I'm supposed to bring it into scope.

How can I get the call to rollback to compile in this code? Where is the missing implicit Origin?

import cats.effect.IO
import munit.CatsEffectSuite
import org.testcontainers.utility.DockerImageName
import cats.effect.kernel.Resource
import skunk.*
import skunk.implicits.*
import natchez.Trace.Implicits.noop
import munit.catseffect.IOFixture
import cats.implicits.catsSyntaxFlatMapOps

val container: Resource[IO, PostgreSQLContainer] =
  Resource.make
    (for {
      c <- IO(PostgreSQLContainer(
        DockerImageName.parse("postgres:17-alpine")
      ))
      _ <- IO.blocking(c.start())
    } yield c)
    (c => IO.blocking(c.stop()))

val sessionResource: Resource[IO, Session[IO]] =
  container.flatMap { c =>
    Session.single[IO](
      host = "localhost",
      port = 5432,
      user = c.username,
      password = Some(c.password),
      database = c.databaseName
    )
  }

def useWithRollback[T](s: Session[IO])(f: Transaction[IO] => IO[T]) =
  s.transaction.use { xa =>
    for {
      result <- f(xa)
      _ <- xa.rollback()  // fails compilation: needs implicit skunk.util.Origin
    } yield result
  }

class PgIntegrationTests extends CatsEffectSuite:

  val session = ResourceSuiteLocalFixture(
    "skunk session",
    sessionResource
  )

  override def munitFixtures = List(session)

  def withDb[T](f: Transaction[IO] => IO[T]) =
    useWithRollback(session())(f)

The idea is that a test would look something like

  test("something") {
    withDb { xa =>
      for {
        result <- callMyCode(xa)
        _ <- assertIO(result, <expected result>)
      } yield ()
    }
  }

The above fails compilation, in useWithRollback with

[error] 41 |      _ <- xa.rollback()
[error]    |           ^^^^^^^^^^^
[error]    |           None of the overloaded alternatives of method rollback in trait Transaction with types
[error]    |            (implicit o: skunk.util.Origin): cats.effect.IO[skunk.data.Completion]
[error]    |            (savepoint: xa.Savepoint)(implicit o: skunk.util.Origin): cats.effect.IO[skunk.data.Completion]
[error]    |           match arguments ()

from which I gather it's looking for an Origin in scope. How can I bring that Origin into scope? Why doesn't the skunk Transaction page (linked above) have this issue where it calls rollback in its example?

Thanks for your time.

P.S: The Mill build file build.sc:

import mill._, scalalib._

object devmap extends ScalaModule {
  def scalaVersion = "3.5.1"
  def ivyDeps = Agg(
    ivy"org.tpolecat::skunk-core:0.6.4",
    ivy"org.typelevel::cats-effect:3.5.7"
  )

  object test extends ScalaTests with TestModule.Munit {
    def ivyDeps = Agg(
      ivy"org.scalameta::munit::0.7.29",
      ivy"org.typelevel::munit-cats-effect:2.0.0",
      ivy"com.dimafeng::testcontainers-scala-postgresql:0.41.5",
      ivy"org.postgresql:postgresql:42.7.4",
      ivy"com.lihaoyi::pprint:0.9.0"
    )
  }
}

Solution

  • Try with this:

    def useWithRollback[T](s: Session[IO])(f: Transaction[IO] => IO[T]) =
      s.transaction.use { xa =>
        for {
          sp <- xa.savepoint
          result <- f(xa)
          _ <- xa.rollback(sp)
        } yield result
      }