packages feed

hasql-mapping 0.1.0.2 → 0.1.1.0

raw patch · 6 files changed

+190/−1 lines, 6 filesdep +hasql-transactionPVP ok

version bump matches the API change (PVP)

Dependencies added: hasql-transaction

API changes (from Hackage documentation)

+ Hasql.Mapping: class IsSession a
+ Hasql.Mapping: class IsTransaction a
+ Hasql.Mapping.IsSession: class IsSession a where {
+ Hasql.Mapping.IsSession: session :: IsSession a => a -> Session (Result a)
+ Hasql.Mapping.IsSession: type Result a;
+ Hasql.Mapping.IsSession: }
+ Hasql.Mapping.IsStatement: toPipeline :: IsStatement a => a -> Pipeline (Result a)
+ Hasql.Mapping.IsStatement: toSession :: IsStatement a => a -> Session (Result a)
+ Hasql.Mapping.IsStatement: toTransaction :: IsStatement a => a -> Transaction (Result a)
+ Hasql.Mapping.IsTransaction: Read :: Mode
+ Hasql.Mapping.IsTransaction: ReadCommitted :: IsolationLevel
+ Hasql.Mapping.IsTransaction: RepeatableRead :: IsolationLevel
+ Hasql.Mapping.IsTransaction: Serializable :: IsolationLevel
+ Hasql.Mapping.IsTransaction: Write :: Mode
+ Hasql.Mapping.IsTransaction: class IsTransaction a where {
+ Hasql.Mapping.IsTransaction: data IsolationLevel
+ Hasql.Mapping.IsTransaction: data Mode
+ Hasql.Mapping.IsTransaction: isolation :: IsTransaction a => IsolationLevel
+ Hasql.Mapping.IsTransaction: mode :: IsTransaction a => Mode
+ Hasql.Mapping.IsTransaction: toSessionWithUnboundedRetries :: IsTransaction a => a -> Session (Result a)
+ Hasql.Mapping.IsTransaction: toSessionWithoutRetries :: IsTransaction a => a -> Session (Result a)
+ Hasql.Mapping.IsTransaction: transaction :: IsTransaction a => a -> Transaction (Result a)
+ Hasql.Mapping.IsTransaction: type Result a;
+ Hasql.Mapping.IsTransaction: }

Files

CHANGELOG.md view
@@ -1,3 +1,9 @@+# v0.1.1.0++- Added `Hasql.Mapping.IsTransaction` and `Hasql.Mapping.IsSession`, completing the class family alongside `IsScalar` and `IsStatement`.+- Added `IsStatement.toSession`, `IsStatement.toPipeline`, `IsStatement.toTransaction`, `IsTransaction.toSessionWithUnboundedRetries` and `IsTransaction.toSessionWithoutRetries` runners.+- New dependency: `hasql-transaction ^>=1.2.3`.+ # v0.1.0.2  - Added support for Hasql 2.0.
hasql-mapping.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hasql-mapping-version: 0.1.0.2+version: 0.1.1.0 synopsis: SDK for defining modular mappings to databases on top of Hasql description:   SDK for defining mappings to databases using Hasql that promotes modular design.@@ -26,6 +26,7 @@ common base   default-language: Haskell2010   default-extensions:+    AllowAmbiguousTypes     ApplicativeDo     BlockArguments     DefaultSignatures@@ -51,13 +52,16 @@   exposed-modules:     Hasql.Mapping     Hasql.Mapping.IsScalar+    Hasql.Mapping.IsSession     Hasql.Mapping.IsStatement+    Hasql.Mapping.IsTransaction    build-depends:     aeson >=2.0 && <3,     base >=4.12 && <5,     bytestring >=0.10 && <0.13,     hasql ^>=1.10.3 || ^>=2.0,+    hasql-transaction ^>=1.2.3,     iproute >=1.7 && <2,     scientific >=0.3 && <0.4,     text >=1.2 && <3,
src/library/Hasql/Mapping.hs view
@@ -4,8 +4,12 @@ module Hasql.Mapping   ( IsScalar,     IsStatement,+    IsTransaction,+    IsSession,   ) where  import Hasql.Mapping.IsScalar (IsScalar)+import Hasql.Mapping.IsSession (IsSession) import Hasql.Mapping.IsStatement (IsStatement)+import Hasql.Mapping.IsTransaction (IsTransaction)
+ src/library/Hasql/Mapping/IsSession.hs view
@@ -0,0 +1,50 @@+module Hasql.Mapping.IsSession where++import Hasql.Session (Session)++-- |+-- Evidence that a data-structure determines a top-level database operation: one with its own+-- error channel and 'System.IO' capability, the two things a bare 'Hasql.Transaction.Transaction'+-- cannot have.+--+-- A single 'Result' rather than separate success/error associated types. An operation with a+-- domain failure expresses it as @type XResult = Either XError A@, which names the outcome once+-- at the definition rather than in every signature. Separate error/success types would+-- additionally force infallible sessions — bulk loads via 'Hasql.Session.onLibpqConnection',+-- @LISTEN@\/@NOTIFY@, batching through 'Hasql.Session.pipeline' — to write @Error X = Void@ and+-- their callers to match an impossible 'Left'.+--+-- Unlike 'Hasql.Mapping.IsTransaction.IsTransaction', this class needs no runner: 'session'+-- already produces a 'Session'.+--+-- ==== __Example: insert-and-catch__+--+-- Insert-and-catch is preferable to check-then-insert: the latter needs 'Serializable' to be+-- correct and costs an extra round trip, while the former is correct at any isolation level in+-- one. Catching above 'Hasql.Mapping.IsTransaction.toSessionWithoutRetries' is safe because by+-- the time an error escapes the runner, the transaction has already been rolled back:+--+-- > module MusicCatalogueDb.Sessions.RegisterAlbum where+-- >+-- > import Hasql.Mapping.IsSession+-- > import qualified Hasql.Mapping.IsTransaction as IsTransaction+-- > import Prelude+-- >+-- > data RegisterAlbum = RegisterAlbum { ... }+-- >+-- > data RegisterAlbumError = AlbumAlreadyExists+-- >+-- > type RegisterAlbumResult = Either RegisterAlbumError AlbumId+-- >+-- > instance IsSession RegisterAlbum where+-- >   type Result RegisterAlbum = RegisterAlbumResult+-- >   session params =+-- >     catchingSqlState (\case "23505" -> Just AlbumAlreadyExists; _ -> Nothing)+-- >       $ IsTransaction.toSessionWithoutRetries (InsertAlbumWithTracks params.album params.tracks)+--+-- (@catchingSqlState@ is proposed upstream at+-- <https://github.com/nikita-volkov/hasql/issues/322 nikita-volkov/hasql#322> and is not yet+-- part of @hasql@; the shape above is illustrative of how it will compose over this class.)+class IsSession a where+  type Result a+  session :: a -> Session (Result a)
src/library/Hasql/Mapping/IsStatement.hs view
@@ -1,6 +1,9 @@ module Hasql.Mapping.IsStatement where +import qualified Hasql.Pipeline as Pipeline+import qualified Hasql.Session as Session import qualified Hasql.Statement as Statement+import qualified Hasql.Transaction as Transaction  -- | -- Evidence that a data-structure models statement parameters determining the statement and its result type.@@ -53,3 +56,21 @@ class IsStatement a where   type Result a   statement :: Statement.Statement a (Result a)++-- |+-- Runs the statement as a session, the construct one level up the capability ladder. Statements+-- have no retry axis, unlike 'Hasql.Mapping.IsTransaction.IsTransaction's runners, so this is the+-- one bare form.+toSession :: (IsStatement a) => a -> Session.Session (Result a)+toSession a = Session.statement a statement++-- |+-- Runs the statement as a step of a 'Hasql.Mapping.IsTransaction.IsTransaction's 'Transaction.Transaction'.+toTransaction :: (IsStatement a) => a -> Transaction.Transaction (Result a)+toTransaction a = Transaction.statement a statement++-- |+-- Runs the statement as a step of a 'Hasql.Session.Session's 'Pipeline.Pipeline', batching it+-- with the other statements pipelined alongside it instead of round-tripping for each.+toPipeline :: (IsStatement a) => a -> Pipeline.Pipeline (Result a)+toPipeline a = Pipeline.statement a statement
+ src/library/Hasql/Mapping/IsTransaction.hs view
@@ -0,0 +1,104 @@+-- | 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.+module Hasql.Mapping.IsTransaction+  ( IsTransaction (..),+    IsolationLevel (..),+    Mode (..),+    toSessionWithUnboundedRetries,+    toSessionWithoutRetries,+  )+where++import qualified Hasql.Session as Session+import Hasql.Transaction (Transaction)+import Hasql.Transaction.Sessions (IsolationLevel (..), Mode (..))+import qualified Hasql.Transaction.Sessions as Sessions++-- |+-- 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 'Sessions.IsolationLevel'+-- and 'Sessions.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__+--+-- > 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+class IsTransaction a where+  type Result a++  -- |+  -- Defaults to 'Serializable', the conservative choice.+  isolation :: IsolationLevel+  isolation = Serializable++  -- |+  -- Defaults to 'Write', the conservative choice.+  mode :: Mode+  mode = Write++  transaction :: a -> Transaction (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.+toSessionWithUnboundedRetries :: forall a. (IsTransaction a) => a -> Session.Session (Result a)+toSessionWithUnboundedRetries a =+  Sessions.transaction (isolation @a) (mode @a) (transaction a)++-- |+-- Runs the transaction with its declared 'isolation' and 'mode', without retrying it on failure.+toSessionWithoutRetries :: forall a. (IsTransaction a) => a -> Session.Session (Result a)+toSessionWithoutRetries a =+  Sessions.transactionNoRetry (isolation @a) (mode @a) (transaction a)