sqlite-simple-interpolate-0.1.1: src/Database/SQLite/Simple/QQ/Interpolated.hs
{-# LANGUAGE TemplateHaskell #-}
-- | Interpolated SQL queries
module Database.SQLite.Simple.QQ.Interpolated
( isql
, quoteInterpolatedSql
, iquery
, iexecute
, ifold
) where
import Language.Haskell.TH (Exp, Q, appE, listE, sigE, tupE, varE, litE)
import Language.Haskell.TH.Quote (QuasiQuoter (..))
import Language.Haskell.TH.Syntax (Lit(..), lookupValueName)
import Database.SQLite.Simple.ToField (toField)
import Text.Parsec (ParseError)
import Database.SQLite.Simple
import Data.Foldable (foldrM)
import Data.String (fromString)
import Database.SQLite.Simple.QQ.Interpolated.Parser (StringPart (..), parseInterpolated)
-- | Quote a SQL statement with embedded antiquoted expressions.
--
-- The result of the quasiquoter is a tuple, containing the statement string and a list
-- of parameters. For example:
--
-- @[isql|SELECT field FROM table WHERE name = ${map toLower "ELLIOT"} LIMIT ${10}|]@
--
-- produces
--
-- @("SELECT field FROM table WHERE name = ? LIMIT ?", [toField ((map toLower) "ELLIOT"), toField 10])@
--
-- How the parser works:
--
-- Any expression occurring between @${@ and @}@ will be replaced with a @?@
-- and passed as a query parameter.
--
-- Characters preceded by a backslash are treated literally. This enables the
-- inclusion of the literal substring @${@ within your quoted text by writing
-- it as @\\${@. The literal sequence @\\${@ may be written as @\\\\${@.
--
-- Note: This quasiquoter is a wrapper around 'Database.SQLite.Simple.QQ.sql'.
--
-- This quasiquoter only works in expression contexts and will throw an error
-- at compile time if used in any other context.
isql :: QuasiQuoter
isql = QuasiQuoter
{ quoteExp = quoteInterpolatedSql
, quotePat = error "isql quasiquoter does not support usage in patterns"
, quoteType = error "isql quasiquoter does not support usage in types"
, quoteDec = error "isql quasiquoter does not support usage in declarations"
}
combineParts :: [StringPart] -> Q ([Q Exp], [Q Exp])
combineParts = foldrM step ([], [])
where
step subExpr (s, exprs) = case subExpr of
AntiInject e -> injectExpr e (s, exprs)
Lit str -> pure (litE (StringL str) : s, exprs)
Esc c -> pure (litE (StringL [c]) : s, exprs)
AntiParam e -> pure (litE (StringL "?") : s, e : exprs)
injectExpr :: String -> ([Q Exp], [Q Exp]) -> Q ([Q Exp], [Q Exp])
injectExpr name (s, exprs) = do
valueName <- lookupValueName name
case valueName of
Nothing ->
fail $ "Value `" ++ name ++ "` is not in scope"
Just found -> do
pure (varE found : s, exprs)
applySql :: [StringPart] -> Q Exp
applySql parts = do
(queryParts, exps) <- combineParts parts
let queryStr = appE [| fromString :: String -> Query |] $ appE [| concat |] $ listE queryParts
tupE [queryStr, sigE (listE $ map (appE (varE 'toField)) exps) [t| [SQLData] |]]
-- | The internal parser used by 'isql'.
quoteInterpolatedSql :: String -> Q Exp
quoteInterpolatedSql s = either (handleError s) applySql (parseInterpolated s)
handleError :: String -> ParseError -> Q Exp
handleError expStr parseError = error $ mconcat
[ "Failed to parse interpolated expression in string: "
, expStr
, "\n"
, show parseError
]
-- | Invokes 'query' with arguments provided by 'isql'.
-- The result is of type '(Connection -> IO [r])'.
iquery :: QuasiQuoter
iquery = isql { quoteExp = appE [| \(q, qs) c -> query c q qs |] . quoteInterpolatedSql }
-- | Invokes 'execute' with arguments provided by 'isql'
-- The result is of type '(Connection -> IO ())'.
iexecute :: QuasiQuoter
iexecute = isql { quoteExp = appE [| \(q, qs) c -> execute c q qs |] . quoteInterpolatedSql }
-- | Invokes 'fold' with arguments provided by 'isql'.
-- The result is of type 'a -> (a -> row -> IO a) -> Connection -> IO a'.
ifold :: QuasiQuoter
ifold = isql { quoteExp = appE [| \(q, qs) acc f c -> fold c q qs acc f |] . quoteInterpolatedSql }