back/src/Guide/Database/Utils.hs

Summary

Maintainability
Test Coverage
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleInstances #-}

-- | Utilities for working with Postgres.
module Guide.Database.Utils
(
  -- * Generic queries
  -- ** Simple query functions
  queryRowMaybe,
  queryRows,
  execute,
  -- ** General query function
  makeStatement,
  -- ** Typeclasses for encoders and decoders
  ToPostgres (..),
  FromPostgres (..),
  -- ** Row conversion
  ToPostgresParam (..),
  FromPostgresColumn (..),
  ToPostgresParams (..),
  FromPostgresRow (..),
  -- ** One-element row newtypes
  SingleParam (..),
  SingleColumn (..),
)
where

import Imports
import Hasql.Statement
import Data.Functor.Contravariant ((>$<))
import Data.Functor.Contravariant.Divisible (divided, lost, chosen)
import Generics.Eot (toEot, HasEot(..))
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Language.Haskell.TH (stringE)

import qualified Data.Set as Set
import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD

import Guide.Uid

----------------------------------------------------------------------------
-- Query functions
----------------------------------------------------------------------------

-- | A wrapper around 'Statement' using named parameters.
makeStatement
  :: "prepared" :! Bool  -- ^ Whether the query should be prepared (see
                         -- <https://www.postgresql.org/docs/current/sql-prepare.html>).
                         -- Use prepared queries if you perform the same
                         -- query many times per 'Session', e.g. in a loop.
  -> "params" :! HE.Params a  -- ^ How to encode the parameters
  -> "result" :! HD.Result b  -- ^ How to decode the result
  -> ByteString  -- ^ Query
  -> Statement a b
makeStatement
  (arg #prepared -> prepared)
  (arg #params -> params)
  (arg #result -> result)
  sql
  =
  Statement sql params result prepared

-- | Create a 'Statement' fetching a single row from the database:
--
-- @
-- statement :: Statement (Uid Trait, Bool) (Maybe (Uid Trait, Text))
-- statement =
--   [queryRowMaybe|
--     SELECT uid, content
--     FROM traits
--     WHERE uid = $1
--       AND deleted = $2
--   |]
-- @
--
-- The resulting 'Statement' can be executed using 'Hasql.Session.statement'
-- from "Hasql.Session" or 'Hasql.Transaction.statement' from
-- "Hasql.Transaction". If the query contains @$1, $2, ...@ placeholders,
-- you will have to fill them:
--
-- @
-- fetchItem :: Session (Maybe (Uid Trait, Text))
-- fetchItem itemId = Hasql.Session.statement (itemId, False) statement
-- @
--
-- 'queryRowMaybe' uses @'ToPostgresParams' a@ for encoding and
-- @'FromPostgresRow' b@ for decoding. To pass no parameters, use @()@. To
-- pass several parameters or get several columns from the query, either use
-- tuples, or write your own row type and derive instances for it. To pass
-- only one parameter, use 'SingleParam', and to get back only one column,
-- use 'SingleColumn'. The following example demonstrates both:
--
-- @
-- statement :: Statement (Uid Item) (Maybe (Uid Category))
-- statement = dimap SingleParam (fmap fromSingleColumn) $
--   [queryRowMaybe|
--     SELECT category_uid
--     FROM items
--     WHERE uid = $1
--   |]
-- @
--
-- See 'Data.Profunctor.lmap', 'Data.Profunctor.rmap',
-- 'Data.Profunctor.dimap' from "Data.Profunctor".
queryRowMaybe :: QuasiQuoter
queryRowMaybe = QuasiQuoter
  { quoteExp = \sql ->
      [|makeStatement
          (#prepared False)
          (#params toPostgresParams)
          (#result (HD.rowMaybe fromPostgresRow))
          (fromString $(stringE sql))|]
  , quotePat = error "queryRowMaybe: can not be used in patterns"
  , quoteType = error "queryRowMaybe: can not be used in types"
  , quoteDec = error "queryRowMaybe: can not be used in declarations"
  }

-- | Create a 'Statement' fetching many rows from the database.
--
-- Like 'queryRowMaybe', but returns @Statement a [b]@ instead of @Statement
-- a (Maybe b)@.
queryRows :: QuasiQuoter
queryRows = QuasiQuoter
  { quoteExp = \sql ->
      [|makeStatement
          (#prepared False)
          (#params toPostgresParams)
          (#result (HD.rowList fromPostgresRow))
          (fromString $(stringE sql))|]
  , quotePat = error "queryRows: can not be used in patterns"
  , quoteType = error "queryRows: can not be used in types"
  , quoteDec = error "queryRows: can not be used in declarations"
  }

-- | Create a 'Statement' that executes a query without returning anything.
--
-- Like 'queryRowMaybe', but returns @Statement a ()@ instead of @Statement
-- a (Maybe b)@.
-- NOTE: 'execute' consumes only one query at time.
execute :: QuasiQuoter
execute = QuasiQuoter
  { quoteExp = \sql ->
      [|makeStatement
          (#prepared False)
          (#params toPostgresParams)
          (#result HD.noResult)
          (fromString $(stringE sql))|]
  , quotePat = error "execute: can not be used in patterns"
  , quoteType = error "execute: can not be used in types"
  , quoteDec = error "execute: can not be used in declarations"
  }

----------------------------------------------------------------------------
-- ToPostgres and FromPostgres
----------------------------------------------------------------------------

class ToPostgres a where
  -- | Encode a single value to the Postgres format.
  --
  -- If you have a newtype over an existing type supported by 'ToPostgres',
  -- you can write an instance like this:
  --
  -- @
  -- instance ToPostgres Foo where
  --   toPostgres = unFoo '>$<' toPostgres
  -- @
  toPostgres :: HE.Value a

class FromPostgres a where
  -- | Decode a single value from the Postgres format.
  --
  -- If you have a newtype over an existing type supported by
  -- 'FromPostgres', you can write an instance like this:
  --
  -- @
  -- instance FromPostgres Foo where
  --   fromPostgres = Foo '<$>' fromPostgres
  -- @
  fromPostgres :: HD.Value a

instance ToPostgres Bool where
  toPostgres = HE.bool

instance FromPostgres Bool where
  fromPostgres = HD.bool

instance ToPostgres Int32 where
  toPostgres = HE.int4

instance FromPostgres Int32 where
  fromPostgres = HD.int4

instance ToPostgres Text where
  toPostgres = HE.text

instance FromPostgres Text where
  fromPostgres = HD.text

instance ToPostgres UTCTime where
  toPostgres = HE.timestamptz

instance FromPostgres UTCTime where
  fromPostgres = HD.timestamptz

instance ToPostgres (Uid a) where
  toPostgres = uidToText >$< HE.text

instance FromPostgres (Uid a) where
  fromPostgres = Uid <$> HD.text

----------------------------------------------------------------------------
-- ToPostgresParam
----------------------------------------------------------------------------

class ToPostgresParam a where
  -- | Convert a single value to a parameter that can be passed to a query.
  --
  -- Unlike 'toPostgres', this function can deal with nullable parameters
  -- and array parameters, which are not representable as 'HE.Value'.
  toPostgresParam :: HE.Params a

-- | Non-nullable parameters.
instance {-# OVERLAPPABLE #-} ToPostgres a => ToPostgresParam a where
  toPostgresParam = HE.param (HE.nonNullable toPostgres)

-- | Nullable parameters.
instance ToPostgres a => ToPostgresParam (Maybe a) where
  toPostgresParam = HE.param (HE.nullable toPostgres)

-- | Arrays of non-nullable values.
instance ToPostgres a => ToPostgresParam [a] where
  toPostgresParam =
    HE.param (HE.nonNullable (HE.foldableArray (HE.nonNullable toPostgres)))

-- | Arrays of nullable values.
instance ToPostgres a => ToPostgresParam [Maybe a] where
  toPostgresParam =
    HE.param (HE.nonNullable (HE.foldableArray (HE.nullable toPostgres)))

-- | Sets of non-nullable values, represented as arrays.
instance ToPostgres a => ToPostgresParam (Set a) where
  toPostgresParam =
    HE.param (HE.nonNullable (HE.foldableArray (HE.nonNullable toPostgres)))

-- | Sets of nullable values, represented as arrays.
instance ToPostgres a => ToPostgresParam (Set (Maybe a)) where
  toPostgresParam =
    HE.param (HE.nonNullable (HE.foldableArray (HE.nullable toPostgres)))

----------------------------------------------------------------------------
-- FromPostgresColumn
----------------------------------------------------------------------------

class FromPostgresColumn a where
  -- | Fetch a single column from a row.
  --
  -- Unlike 'fromPostgres', this function can deal with nullable columns and
  -- array columns, which are not representable as 'HD.Value'.
  fromPostgresColumn :: HD.Row a

-- | Non-nullable columns.
instance {-# OVERLAPPABLE #-} FromPostgres a => FromPostgresColumn a where
  fromPostgresColumn = HD.column (HD.nonNullable fromPostgres)

-- | Nullable columns.
instance FromPostgres a => FromPostgresColumn (Maybe a) where
  fromPostgresColumn = HD.column (HD.nullable fromPostgres)

-- | Arrays of non-nullable values.
instance FromPostgres a => FromPostgresColumn [a] where
  fromPostgresColumn =
    HD.column (HD.nonNullable (HD.listArray (HD.nonNullable fromPostgres)))

-- | Arrays of nullable values.
instance FromPostgres a => FromPostgresColumn [Maybe a] where
  fromPostgresColumn =
    HD.column (HD.nonNullable (HD.listArray (HD.nullable fromPostgres)))

-- | Sets of non-nullable values, represented as arrays.
instance (Ord a, FromPostgres a) => FromPostgresColumn (Set a) where
  fromPostgresColumn =
    Set.fromList <$>
    HD.column (HD.nonNullable (HD.listArray (HD.nonNullable fromPostgres)))

-- | Sets of nullable values, represented as arrays.
instance (Ord a, FromPostgres a) => FromPostgresColumn (Set (Maybe a)) where
  fromPostgresColumn =
    Set.fromList <$>
    HD.column (HD.nonNullable (HD.listArray (HD.nullable fromPostgres)))

----------------------------------------------------------------------------
-- ToPostgresParams
----------------------------------------------------------------------------

class ToPostgresParams a where
  -- | Pass a row of parameters to a query.
  toPostgresParams :: HE.Params a

  -- | A default implementation for anything that implements 'Generic'.
  default toPostgresParams :: (HasEot a, GToPostgresParams (Eot a)) => HE.Params a
  toPostgresParams = toEot >$< genericToPostgresParams

class GToPostgresParams a where
  -- | A generic method to encode data types with one constructor as Hasql
  -- rows. See
  -- <https://generics-eot.readthedocs.io/en/stable/tutorial.html#eot-isomorphic-representations>
  -- to understand its implementation.
  genericToPostgresParams :: HE.Params a

-- An instance for zero fields.
instance GToPostgresParams () where
  genericToPostgresParams = HE.noParams

-- If we can encode N fields, we can encode N+1 fields.
instance (ToPostgresParam a, GToPostgresParams b) => GToPostgresParams (a, b) where
  genericToPostgresParams = divided toPostgresParam genericToPostgresParams

-- One-constructor types are represented as @Either a Void@, where @a@ is a
-- tuple that we already know how to encode thanks to the instances above.
-- We do not support types with more than one constructor.
instance GToPostgresParams a => GToPostgresParams (Either a Void) where
  genericToPostgresParams = chosen genericToPostgresParams lost

----------------------------------------------------------------------------
-- FromPostgresRow
----------------------------------------------------------------------------

class FromPostgresRow a where
  -- | Fetch a row of results from a query.
  fromPostgresRow :: HD.Row a

  -- | A default implementation for anything that implements 'Generic'.
  default fromPostgresRow :: (HasEot a, GFromPostgresRow (Eot a)) => HD.Row a
  fromPostgresRow = fromEot <$> genericFromPostgresRow

class GFromPostgresRow a where
  -- | A generic method to decode data types with one constructor as Hasql
  -- rows. See
  -- <https://generics-eot.readthedocs.io/en/stable/tutorial.html#eot-isomorphic-representations>
  -- to understand its implementation.
  genericFromPostgresRow :: HD.Row a

-- An instance for zero fields.
instance GFromPostgresRow () where
  genericFromPostgresRow = pure ()

-- If we can encode N fields, we can encode N+1 fields.
instance (FromPostgresColumn a, GFromPostgresRow b) => GFromPostgresRow (a, b) where
  genericFromPostgresRow = (,) <$> fromPostgresColumn <*> genericFromPostgresRow

-- One-constructor types are represented as @Either a Void@, where @a@ is a
-- tuple that we already know how to encode thanks to the instances above.
-- We do not support types with more than one constructor.
instance GFromPostgresRow a => GFromPostgresRow (Either a Void) where
  genericFromPostgresRow = Left <$> genericFromPostgresRow

----------------------------------------------------------------------------
-- SingleParam and SingleColumn
----------------------------------------------------------------------------

newtype SingleParam a = SingleParam { fromSingleParam :: a }
  deriving (Eq, Ord, Show, Generic)

instance ToPostgresParam a => ToPostgresParams (SingleParam a)

newtype SingleColumn a = SingleColumn { fromSingleColumn :: a }
  deriving (Eq, Ord, Show, Generic)

instance FromPostgresColumn a => FromPostgresRow (SingleColumn a)

----------------------------------------------------------------------------
-- Tuples
----------------------------------------------------------------------------

-- Note: 'Generic' provides instances for tuples only until 7-tuples

instance ToPostgresParams ()
instance (ToPostgresParam a, ToPostgresParam b) => ToPostgresParams (a, b)
instance (ToPostgresParam a, ToPostgresParam b, ToPostgresParam c) => ToPostgresParams (a, b, c)
instance (ToPostgresParam a, ToPostgresParam b, ToPostgresParam c, ToPostgresParam d) => ToPostgresParams (a, b, c, d)
instance (ToPostgresParam a, ToPostgresParam b, ToPostgresParam c, ToPostgresParam d, ToPostgresParam e) => ToPostgresParams (a, b, c, d, e)
instance (ToPostgresParam a, ToPostgresParam b, ToPostgresParam c, ToPostgresParam d, ToPostgresParam e, ToPostgresParam f) => ToPostgresParams (a, b, c, d, e, f)
instance (ToPostgresParam a, ToPostgresParam b, ToPostgresParam c, ToPostgresParam d, ToPostgresParam e, ToPostgresParam f, ToPostgresParam g) => ToPostgresParams (a, b, c, d, e, f, g)

instance FromPostgresRow ()
instance (FromPostgresColumn a, FromPostgresColumn b) => FromPostgresRow (a, b)
instance (FromPostgresColumn a, FromPostgresColumn b, FromPostgresColumn c) => FromPostgresRow (a, b, c)
instance (FromPostgresColumn a, FromPostgresColumn b, FromPostgresColumn c, FromPostgresColumn d) => FromPostgresRow (a, b, c, d)
instance (FromPostgresColumn a, FromPostgresColumn b, FromPostgresColumn c, FromPostgresColumn d, FromPostgresColumn e) => FromPostgresRow (a, b, c, d, e)
instance (FromPostgresColumn a, FromPostgresColumn b, FromPostgresColumn c, FromPostgresColumn d, FromPostgresColumn e, FromPostgresColumn f) => FromPostgresRow (a, b, c, d, e, f)
instance (FromPostgresColumn a, FromPostgresColumn b, FromPostgresColumn c, FromPostgresColumn d, FromPostgresColumn e, FromPostgresColumn f, FromPostgresColumn g) => FromPostgresRow (a, b, c, d, e, f, g)