packages feed

hasql-th-0.5.1: src/library/Hasql/TH.hs

module Hasql.TH
  ( -- * Statements

    -- |
    --  Quasiquoters in this category produce Hasql `Statement`s,
    --  checking the correctness of SQL at compile-time.
    --
    --  To extract the information about parameters and results of the statement,
    --  the quoter requires you to explicitly specify the Postgres types for placeholders and results.
    --
    --  Here's an example of how to use it:
    --
    --  Enable the @QuasiQuotes@ language extension in any module that uses
    --  these quasiquoters:
    --
    --  >{-# LANGUAGE QuasiQuotes #-}
    --
    --  >selectUserDetails :: Statement Int32 (Maybe (Text, Text, Maybe Text))
    --  >selectUserDetails =
    --  >  [maybeStatement|
    --  >    select name :: text, email :: text, phone :: text?
    --  >    from "user"
    --  >    where id = $1 :: int4
    --  >    |]
    --
    --  As you can see, it completely eliminates the need to mess with codecs.
    --  The quasiquoters directly produce `Statement`,
    --  which you can then `Data.Profunctor.dimap` over using its `Data.Profunctor.Profunctor` instance to get to your domain types.
    --
    --  == Type mappings
    --
    --  === Primitives
    --
    --  Following is a list of supported Postgres types and their according types on the Haskell end.
    --
    --  - @bool@ - `Bool`
    --  - @int2@ - `Int16`
    --  - @int4@ - `Int32`
    --  - @int8@ - `Int64`
    --  - @float4@ - `Float`
    --  - @float8@ - `Double`
    --  - @numeric@ - `Data.Scientific.Scientific`
    --  - @char@ - `Char`
    --  - @text@ - `Data.Text.Text`
    --  - @bytea@ - `Data.ByteString.ByteString`
    --  - @date@ - `Data.Time.Day`
    --  - @timestamp@ - `Data.Time.LocalTime`
    --  - @timestamptz@ - `Data.Time.UTCTime`
    --  - @time@ - `Data.Time.TimeOfDay`
    --  - @timetz@ - @(`Data.Time.TimeOfDay`, `Data.Time.TimeZone`)@
    --  - @interval@ - `Data.Time.DiffTime`
    --  - @uuid@ - `Data.UUID.UUID`
    --  - @inet@ - @(`Network.IP.Addr.NetAddr` `Network.IP.Addr.IP`)@
    --  - @json@ - `Data.Aeson.Value`
    --  - @jsonb@ - `Data.Aeson.Value`
    --
    --  === Arrays
    --
    --  Array mappings are also supported.
    --  They are specified according to Postgres syntax: by appending one or more @[]@ to the primitive type,
    --  depending on how many dimensions the array has.
    --  On the Haskell end array is mapped to generic `Data.Vector.Generic.Vector`,
    --  allowing you to choose which particular vector implementation to map to.
    --
    --  === Nulls
    --
    --  As you might have noticed in the example,
    --  we introduce one change to the Postgres syntax in the way
    --  the typesignatures are parsed:
    --  we interpret question-marks in them as specification of nullability.
    --  Here's more examples of that:
    --
    --  >>> :t [singletonStatement| select a :: int4? |]
    --  ...
    --    :: Statement () (Maybe Int32)
    --
    --  You can use it to specify the nullability of array elements:
    --
    --  >>> :t [singletonStatement| select a :: int4?[] |]
    --  ...
    --    :: Data.Vector.Generic.Base.Vector v (Maybe Int32) =>
    --       Statement () (v (Maybe Int32))
    --
    --  And of arrays themselves:
    --
    --  >>> :t [singletonStatement| select a :: int4?[]? |]
    --  ...
    --    :: Data.Vector.Generic.Base.Vector v (Maybe Int32) =>
    --       Statement () (Maybe (v (Maybe Int32)))

    -- ** Row-parsing statements
    singletonStatement,
    maybeStatement,
    vectorStatement,
    foldStatement,

    -- ** Row-ignoring statements
    resultlessStatement,
    rowsAffectedStatement,

    -- * SQL Strings

    -- |
    -- Text-producing quasiquoters performing no compile-time checking.
    uncheckedSql,
    uncheckedSqlFile,
  )
where

import qualified Data.Text as Text
import qualified Hasql.TH.Construction.Exp as Exp
import qualified Hasql.TH.Extraction.Exp as ExpExtraction
import Hasql.TH.Prelude hiding (exp)
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax
import qualified PostgresqlSyntax as Ast

-- * Helpers

exp :: (String -> Q Exp) -> QuasiQuoter
exp =
  let unsupported _ = fail "Unsupported"
   in \exp -> QuasiQuoter exp unsupported unsupported unsupported

expParser :: (Text -> Either Text Exp) -> QuasiQuoter
expParser parser =
  exp $ \inputString -> either (fail . Text.unpack) return $ parser $ fromString inputString

expPreparableStmtAstParser :: (Ast.PreparableStmt -> Either Text Exp) -> QuasiQuoter
expPreparableStmtAstParser parser =
  expParser $ \input -> do
    ast <- Ast.parse settings input
    parser ast
  where
    settings =
      mconcat
        [ Ast.nullabilityMarkers True
        ]

-- * Statement

-- |
-- @
-- :: `Statement` params row
-- @
--
-- Statement producing exactly one result row.
--
-- Will cause the running session to fail with the
-- `Hasql.Session.UnexpectedAmountOfRows` error if it's any other.
--
-- === __Examples__
--
-- >>> :t [singletonStatement|select 1 :: int2|]
-- ... :: Statement () Int16
--
-- >>> :{
--   :t [singletonStatement|
--        insert into "user" (email, name)
--        values ($1 :: text, $2 :: text)
--        returning id :: int4
--        |]
-- :}
-- ...
-- ... :: Statement (Text, Text) Int32
--
-- Incorrect SQL:
--
-- >>> :t [singletonStatement|elect 1|]
-- ...
--   |
-- 1 | elect 1
--   | ^
-- ...
singletonStatement :: QuasiQuoter
singletonStatement = expPreparableStmtAstParser (ExpExtraction.undecodedStatement Exp.singleRowResultDecoder)

-- |
-- @
-- :: `Statement` params (Maybe row)
-- @
--
-- Statement producing one row or none.
--
-- === __Examples__
--
-- >>> :t [maybeStatement|select 1 :: int2|]
-- ... :: Statement () (Maybe Int16)
maybeStatement :: QuasiQuoter
maybeStatement = expPreparableStmtAstParser (ExpExtraction.undecodedStatement Exp.rowMaybeResultDecoder)

-- |
-- @
-- :: `Statement` params (`Vector` row)
-- @
--
-- Statement producing a vector of rows.
--
-- === __Examples__
--
-- >>> :t [vectorStatement|select 1 :: int2|]
-- ... :: Statement () (Vector Int16)
vectorStatement :: QuasiQuoter
vectorStatement = expPreparableStmtAstParser (ExpExtraction.undecodedStatement Exp.rowVectorResultDecoder)

-- |
-- @
-- :: `Fold` row folding -> `Statement` params folding
-- @
--
-- Function from `Fold` over rows to a statement producing the result of folding.
-- Use this when you need to aggregate rows customly.
--
-- === __Examples__
--
-- >>> :t [foldStatement|select 1 :: int2|]
-- ... :: Fold Int16 b -> Statement () b
foldStatement :: QuasiQuoter
foldStatement = expPreparableStmtAstParser ExpExtraction.foldStatement

-- |
-- @
-- :: `Statement` params ()
-- @
--
-- Statement producing no results.
--
-- === __Examples__
--
-- >>> :t [resultlessStatement|insert into "user" (name, email) values ($1 :: text, $2 :: text)|]
-- ...
-- ... :: Statement (Text, Text) ()
resultlessStatement :: QuasiQuoter
resultlessStatement = expPreparableStmtAstParser (ExpExtraction.undecodedStatement (const Exp.noResultResultDecoder))

-- |
-- @
-- :: `Statement` params Int64
-- @
--
-- Statement counting the rows it affects.
--
-- === __Examples__
--
-- >>> :t [rowsAffectedStatement|delete from "user" where password is null|]
-- ...
-- ... :: Statement () Int64
rowsAffectedStatement :: QuasiQuoter
rowsAffectedStatement = expPreparableStmtAstParser (ExpExtraction.undecodedStatement (const Exp.rowsAffectedResultDecoder))

-- * SQL Strings

-- |
-- Quoter of a multiline Unicode SQL string,
-- which gets converted into a format ready to be used for declaration of statements.
uncheckedSql :: QuasiQuoter
uncheckedSql = exp $ return . Exp.text . fromString

-- |
-- Read an SQL-file, containing multiple statements,
-- and produce an expression of type 'Text'.
--
-- Allows to store plain SQL in external files and read it at compile time.
--
-- E.g.,
--
-- >migration1 :: Hasql.Session.Session ()
-- >migration1 = Hasql.Session.script [uncheckedSqlFile|migrations/1.sql|]
uncheckedSqlFile :: QuasiQuoter
uncheckedSqlFile = quoteFile uncheckedSql

-- * Tests

-- $
-- >>> :t [maybeStatement| select (password = $2 :: text) :: bool, id :: int4 from "user" where "email" = $1 :: text |]
-- ...
-- ... Statement (Text, Text) (Maybe (Bool, Int32))
--
-- >>> :t [maybeStatement| select id :: int4 from application where pub_key = $1 :: uuid and sec_key_pt1 = $2 :: int8 and sec_key_pt2 = $3 :: int8 |]
-- ...
-- ... Statement (UUID, Int64, Int64) (Maybe Int32)
--
-- >>> :t [singletonStatement| select 1 :: int4 from a left join b on b.id = a.id |]
-- ...
-- ... Statement () Int32