| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Hasql.Mapping
Description
Reexports of classes without methods.
To access the methods import the class-specific modules preferably qualified.
Synopsis
- class IsScalar a
- class IsStatement a
- class IsTransaction a
- class IsSession a
Documentation
Mapping to a scalar value. Anything but array.
The current Hasql API doesn't provide a typesafe boundary to enforce the value being scalar, so consider this to be a part of the contract to not define instances for array mappings using this class.
Instances
| IsScalar Value # | Maps to PostgreSQL |
| IsScalar ByteString # | Maps to PostgreSQL |
Defined in Hasql.Mapping.IsScalar | |
| IsScalar Int16 # | Maps to PostgreSQL |
| IsScalar Int32 # | Maps to PostgreSQL |
| IsScalar Int64 # | Maps to PostgreSQL |
| IsScalar IPRange # | Maps to PostgreSQL |
| IsScalar Scientific # | Maps to PostgreSQL |
Defined in Hasql.Mapping.IsScalar | |
| IsScalar Text # | Maps to PostgreSQL |
| IsScalar Day # | Maps to PostgreSQL |
| IsScalar DiffTime # | Maps to PostgreSQL |
| IsScalar UTCTime # | Maps to PostgreSQL |
| IsScalar LocalTime # | Maps to PostgreSQL |
| IsScalar TimeOfDay # | Maps to PostgreSQL |
| IsScalar UUID # | Maps to PostgreSQL |
| IsScalar Bool # | Maps to PostgreSQL |
| IsScalar Double # | Maps to PostgreSQL |
| IsScalar Float # | Maps to PostgreSQL |
| IsScalar Int # | Maps to PostgreSQL |
| IsScalar (TimeOfDay, TimeZone) # | Maps to PostgreSQL |
| IsScalar (Word8, Word8, Word8, Word8, Word8, Word8) # | Maps to PostgreSQL |
class IsStatement a #
Evidence that a data-structure models statement parameters determining the statement and its result type.
Supports a modularisation pattern, where you define everything related to one statement in an isolated module. This pattern leads to high code cohesion and low coupling.
Example of such a module
module MusicCatalogueDb.Statements.SelectArtistIdsByName where
import Data.Functor.Contravariant
import Data.Text (Text)
import Data.UUID (UUID)
import Data.Vector (Vector)
import qualified Hasql.Decoders as Decoders
import qualified Hasql.Encoders as Encoders
import Hasql.Mapping.IsStatement
import Prelude
data SelectArtistIdsByName = SelectArtistIdsByName
{ name :: Text
}
type SelectArtistIdsByNameResult = Vector SelectArtistIdsByNameResultRow
data SelectArtistIdsByNameResultRow = SelectArtistIdsByNameResultRow
{ id :: UUID
}
instance IsStatement SelectArtistIdsByName where
type Result SelectArtistIdsByName = SelectArtistIdsByNameResult
statement =
Statement.preparable sql encoder decoder
where
sql =
"select id from artist\n\
\where name = $1\n\
\limit 1"
encoder =
mconcat
[ (\(SelectArtistIdsByName x) -> x)
>$< Encoders.param (Encoders.nonNullable Encoders.text)
]
decoder =
Decoders.rowVector
( SelectArtistIdsByNameResultRow
<$> Decoders.column (Decoders.nonNullable Decoders.uuid)
)Minimal complete definition
class IsTransaction a #
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:
memptyisminBound(ReadCommittedandRead), so that a component with no opinion never downgrades a component that has one.- An omitted class method defaults to
SerializableandWrite, 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 albumIdMinimal complete definition
Evidence that a data-structure determines a top-level database operation: one with its own
error channel and IO capability, the two things a bare 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 onLibpqConnection,
LISTEN/NOTIFY, batching through pipeline — to write Error X = Void and
their callers to match an impossible Left.
Unlike 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 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
nikita-volkov/hasql#322 and is not yet
part of hasql; the shape above is illustrative of how it will compose over this class.)
Minimal complete definition