All Downloads are FREE. Search and download functionalities are using the official Maven repository.

simosiani.skunk-core_3.0.3.2.source-code.Transaction.scala Maven / Gradle / Ivy

// Copyright (c) 2018-2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package skunk

import cats._
import cats.effect.Resource
import cats.effect.Resource.ExitCase
import cats.effect.Resource.ExitCase._
import cats.syntax.all._
import skunk.implicits._
import skunk.data.Completion
import skunk.data.TransactionStatus._
import skunk.util.Origin
import skunk.util.CallSite
import skunk.exception.SkunkException
import skunk.util.Namer
import skunk.data.TransactionStatus
import skunk.data.TransactionIsolationLevel
import skunk.data.TransactionAccessMode

/**
 * Control methods for use within a `transaction` block. An instance is provided when you call
 * `Session.transaction(...).use`.
 * @see Session#transaction for information on default commit/rollback behavior
 *
 * @groupname XA Transaction Control
 */
trait Transaction[F[_]] { outer =>

  /** Existential type for savepoints within this transaction block. */
  type Savepoint

  /**
   * Current transaction status. It is not usually necessary to check because transactions will be
   * committed or rolled back automatically, but if you are committing manually and your logic is
   * sufficiently complex it may be helpful.
   * @group XA
   */
  def status: F[TransactionStatus]

  /**
   * Create a `Savepoint`, to which you can later roll back.
   * @group XA
   */
  def savepoint(implicit o: Origin): F[Savepoint]

  /**
   * Roll back to the specified `Savepoint`, leaving the transaction active at that point.
   * @group XA
   */
  def rollback(savepoint: Savepoint)(implicit o: Origin): F[Completion]

  /**
   * Terminate the transaction by rolling back. This is normally not necessary because a transaction
   * will be rolled back automatically when the block exits abnormally.
   * @see Session#transaction for information on default commit/rollback behavior
   * @group XA
   */
  def rollback(implicit o: Origin): F[Completion]

  /**
   * Terminate the transaction by committing early. This is normally not necessary because a
   * transaction will be committed automatically if the block exits successfully.
   * @see Session#transaction for information on default commit/rollback behavior
   * @group XA
   */
  def commit(implicit o: Origin): F[Completion]

  /**
   * Transform this `Transaction` by a given `FunctionK`.
   * @group Transformations
   */
  def mapK[G[_]](fk: F ~> G): Transaction[G] =
    new Transaction[G] {
      override type Savepoint = outer.Savepoint
      override def commit(implicit o: Origin): G[Completion] = fk(outer.commit)
      override def rollback(implicit o: Origin): G[Completion] = fk(outer.rollback)
      override def rollback(savepoint: Savepoint)(implicit o: Origin): G[Completion] = fk(outer.rollback(savepoint))
      override def savepoint(implicit o: Origin): G[Savepoint] = fk(outer.savepoint)
      override def status: G[TransactionStatus] = fk(outer.status)
    }

}

object Transaction {

  def fromSession[F[_]](
    s: Session[F],
    n: Namer[F],
    // o: Origin // origin of the call to .begin
    iOp: Option[TransactionIsolationLevel],
    aOp: Option[TransactionAccessMode]
  )(
    implicit ev: MonadError[F, Throwable]
  ): Resource[F, Transaction[F]] = {

    def assertIdle(cs: CallSite): F[Unit] =
      s.transactionStatus.get.flatMap {
        case Idle              => ().pure[F]
        case Active =>
          new SkunkException(
            sql      = None,
            message  = "Nested transactions are not allowed.",
            hint     = Some("You must roll back or commit the current transaction before starting a new one."),
            callSite = Some(cs)
          ).raiseError[F, Unit]
        case Failed =>
          new SkunkException(
            sql      = None,
            message  = "Nested transactions are not allowed.",
            hint     = Some("You must roll back the current (failed) transaction before starting a new one."),
            callSite = Some(cs)
          ).raiseError[F, Unit]
      }

    def assertActive(cs: CallSite): F[Unit] =
      s.transactionStatus.get.flatMap {
        case Idle   =>
          new SkunkException(
            sql      = None,
            message  = "No transaction.",
            hint     = Some("The transaction has already been committed or rolled back."),
            callSite = Some(cs)
          ).raiseError[F, Unit]
        case Active => ().pure[F]
        case Failed =>
          new SkunkException(
            sql      = None,
            message  = "Transaction has failed.",
            hint     = Some("""
              |The active transaction has failed and needs to be rolled back (either entirely or to
              |a prior savepoint) before you can continue. The most common explanation is that
              |Postgres raised an error earlier in the transaction and you handled it in your
              |application code, but you forgot to roll back.
            """.trim.stripMargin.replace('\n', ' ')),
            callSite = Some(cs)
          ).raiseError[F, Unit]
      }

    def assertActiveOrError(cs: CallSite): F[Unit] =
      cs.pure[F].void

    def doRollback: F[Completion] =
      s.execute(internal"ROLLBACK".command)

    def doCommit: F[Completion] =
      s.execute(internal"COMMIT".command)

    val acquire: F[Transaction[F]] =
      assertIdle(CallSite("begin", Origin.unknown)) *>
      s.execute(
        internal"""BEGIN
                  ${iOp.foldMap(i => s"ISOLATION LEVEL ${i.sql}")}
                  ${aOp.foldMap(_.sql)}""".command
      ).map { _ =>
        new Transaction[F] {

          override type Savepoint = String

          override def status: F[TransactionStatus] =
            s.transactionStatus.get

          override def commit(implicit o: Origin): F[Completion] =
            assertActive(o.toCallSite("commit")) *>
            doCommit

          override def rollback(implicit o: Origin): F[Completion] =
            assertActiveOrError(o.toCallSite("rollback")) *>
            doRollback

          override def rollback(savepoint: Savepoint)(implicit o: Origin): F[Completion] =
            assertActiveOrError(o.toCallSite("savepoint")) *>
            s.execute(internal"ROLLBACK TO ${savepoint}".command)

          override def savepoint(implicit o: Origin): F[Savepoint] =
            for {
              _ <- assertActive(o.toCallSite("savepoint"))
              i <- n.nextName("savepoint")
              _ <- s.execute(internal"SAVEPOINT $i".command)
            } yield i

        }
      }

    val release: (Transaction[F], ExitCase) => F[Unit] = (_, ec) =>
      s.transactionStatus.get.flatMap {
        case Idle              =>
          // This means the user committed manually, so there's nothing to do
          ().pure[F]
        case Failed =>
          ec match {
            // This is the normal failure case
            case Errored(t)  => doRollback *> t.raiseError[F, Unit]
            // This is possible if you swallow an error
            case Succeeded => doRollback.void
            // This is possible if you swallow an error and the someone cancels the fiber
            case Canceled  => doRollback.void
          }
        case Active =>
          ec match {
            // This is the normal success case
            case Succeeded => doCommit.void
            // If someone cancels the fiber we roll back
            case Canceled  => doRollback.void
            // If an error escapes we roll back
            case Errored(t)  => doRollback *> t.raiseError[F, Unit]
          }
      }

    Resource.makeCase(acquire)(release)

  }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy