hasql-mapping
Safe HaskellNone
LanguageHaskell2010

Hasql.Mapping.IsTransaction

Description

An explicit export list is required here (unlike this package's other modules) purely mechanically: a module with no export list only exports entities it defines, not ones it merely imports, so IsolationLevel and Mode would otherwise stay invisible to any instance that imports only this module.

Synopsis

Documentation

class IsTransaction a where #

Evidence that a data-structure determines an atomic, retryable database transaction.

isolation and mode are properties of the transaction, not of the call site: whether an operation needs Serializable is a fact about what it does, and a caller reaching for toSessionWithUnboundedRetries or toSessionWithoutRetries cannot override or forget them.

The defaults are the conservative ones (Serializable and Write), so the safe case is free and every relaxation is explicit and reviewable in the instance. The opposite defaults would make an under-isolated transaction invisible.

A composite transaction declares the join of its components by hand, using IsolationLevel and Mode's Semigroup instances:

instance IsTransaction Composite where
  isolation = isolation \@Part1 <> isolation \@Part2
  mode = mode \@Part1 <> mode \@Part2

The two identities are deliberately opposite, because a reader who learns one will guess the other wrong:

  • mempty is minBound (ReadCommitted and Read), so that a component with no opinion never downgrades a component that has one.
  • An omitted class method defaults to Serializable and Write, so that an author who never considered the question gets the safe answer.

Both are conservative, by opposite rules. The join is declared explicitly rather than derived, so changing which components make up a composite requires updating these declarations as well.

Example of such a module

Expand
module MusicCatalogueDb.Transactions.InsertAlbumWithTracks where

import Hasql.Mapping.IsTransaction
import qualified Hasql.Transaction as Transaction
import qualified MusicCatalogueDb.Statements.InsertAlbum as InsertAlbum
import qualified MusicCatalogueDb.Statements.InsertTrack as InsertTrack
import Prelude

data InsertAlbumWithTracks = InsertAlbumWithTracks
  { album :: InsertAlbum.InsertAlbum,
    tracks :: [InsertTrack.InsertTrack]
  }

type InsertAlbumWithTracksResult = InsertAlbum.InsertAlbumResult

instance IsTransaction InsertAlbumWithTracks where
  type Result InsertAlbumWithTracks = InsertAlbumWithTracksResult
  isolation = ReadCommitted -- inserts only fresh rows, so no anomaly exposure
  transaction params = do
    albumId <- Transaction.statement params.album InsertAlbum.statement
    for_ params.tracks \track ->
      Transaction.statement track InsertTrack.statement
    pure albumId

Minimal complete definition

transaction

Associated Types

type Result a #

Methods

isolation :: IsolationLevel #

Defaults to Serializable, the conservative choice.

mode :: Mode #

Defaults to Write, the conservative choice.

transaction :: a -> Transaction (Result a) #

data IsolationLevel #

For reference see the Postgres' documentation.

Instances

Instances details
Monoid IsolationLevel # 
Instance details

Defined in Hasql.Transaction.Config

Semigroup IsolationLevel #

Combines two isolation levels by picking the stricter one.

mempty is ReadCommitted, the identity of max: it never overrides an explicit stricter requirement, so a piece of a composed transaction that needs e.g. Serializable always wins over pieces that don't care.

Instance details

Defined in Hasql.Transaction.Config

Eq IsolationLevel # 
Instance details

Defined in Hasql.Transaction.Config

Ord IsolationLevel # 
Instance details

Defined in Hasql.Transaction.Config

Bounded IsolationLevel # 
Instance details

Defined in Hasql.Transaction.Config

Enum IsolationLevel # 
Instance details

Defined in Hasql.Transaction.Config

Show IsolationLevel # 
Instance details

Defined in Hasql.Transaction.Config

data Mode #

Constructors

Read

Read-only. No writes possible.

Write

Write and commit.

Instances

Instances details
Monoid Mode # 
Instance details

Defined in Hasql.Transaction.Config

Methods

mempty :: Mode #

mappend :: Mode -> Mode -> Mode #

mconcat :: [Mode] -> Mode #

Semigroup Mode #

Combines two modes by picking the one that grants more capability.

mempty is Read, the identity of max: it never overrides an explicit Write requirement, so a piece of a composed transaction that needs to write always wins over pieces that don't care.

Instance details

Defined in Hasql.Transaction.Config

Methods

(<>) :: Mode -> Mode -> Mode #

sconcat :: NonEmpty Mode -> Mode #

stimes :: Integral b => b -> Mode -> Mode #

Eq Mode # 
Instance details

Defined in Hasql.Transaction.Config

Methods

(==) :: Mode -> Mode -> Bool #

(/=) :: Mode -> Mode -> Bool #

Ord Mode # 
Instance details

Defined in Hasql.Transaction.Config

Methods

compare :: Mode -> Mode -> Ordering #

(<) :: Mode -> Mode -> Bool #

(<=) :: Mode -> Mode -> Bool #

(>) :: Mode -> Mode -> Bool #

(>=) :: Mode -> Mode -> Bool #

max :: Mode -> Mode -> Mode #

min :: Mode -> Mode -> Mode #

Bounded Mode # 
Instance details

Defined in Hasql.Transaction.Config

Enum Mode # 
Instance details

Defined in Hasql.Transaction.Config

Methods

succ :: Mode -> Mode #

pred :: Mode -> Mode #

toEnum :: Int -> Mode #

fromEnum :: Mode -> Int #

enumFrom :: Mode -> [Mode] #

enumFromThen :: Mode -> Mode -> [Mode] #

enumFromTo :: Mode -> Mode -> [Mode] #

enumFromThenTo :: Mode -> Mode -> Mode -> [Mode] #

Show Mode # 
Instance details

Defined in Hasql.Transaction.Config

Methods

showsPrec :: Int -> Mode -> ShowS #

show :: Mode -> String #

showList :: [Mode] -> ShowS #

toSessionWithUnboundedRetries :: IsTransaction a => a -> Session (Result a) #

Runs the transaction with its declared isolation and mode, retrying it indefinitely on serialization failures and deadlocks.

hasql-transaction's retry is a fix loop with no backoff and no cap: a Serializable transaction under sustained contention can spin indefinitely, holding a connection and never surfacing an error. Prefer toSessionWithoutRetries with your own bounded retry loop unless that is an acceptable risk for the operation.

toSessionWithoutRetries :: IsTransaction a => a -> Session (Result a) #

Runs the transaction with its declared isolation and mode, without retrying it on failure.