selda-0.5.2.0: src/Database/Selda/Prepared.hs
{-# LANGUAGE TypeFamilies, FlexibleInstances, ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Building and executing prepared statements.
module Database.Selda.Prepared (Preparable, Prepare, prepared) where
import Database.Selda.Backend.Internal
( Lit(LCustom),
SqlType(sqlType),
SqlTypeRep,
Param(..),
MonadSelda(Backend, withConnection),
SeldaBackend(ppConfig, runPrepared, backendId, prepareStmt),
SeldaConnection(connBackend, connStmts),
SeldaStmt(SeldaStmt, stmtHandle, stmtParams, stmtText),
StmtID(..),
BackendID,
freshStmtId,
withBackend )
import Database.Selda.Column ( Exp(Lit), Col(..) )
import Database.Selda.Compile
( Result, Res, compileWith, buildResult )
import Database.Selda.Query.Type ( Query )
import Database.Selda.SQL (param, paramType)
import Control.Exception ( Exception, try, throw, mask )
import Control.Monad.IO.Class ( MonadIO(liftIO) )
import qualified Data.IntMap as M
import Data.IORef
( IORef, atomicModifyIORef', newIORef, readIORef, writeIORef )
import Data.Proxy ( Proxy(..) )
import Data.Text (Text)
import Data.Typeable ( Typeable )
import System.IO.Unsafe ( unsafePerformIO )
data Placeholder = Placeholder Int
deriving Show
instance Exception Placeholder
-- | Index of first argument parameter to a query.
firstParamIx :: Int
firstParamIx = 0
-- | Result type of a monadic computation.
type family ResultT f where
ResultT (a -> b) = ResultT b
ResultT (m a) = a
type family Equiv q f where
Equiv (Col s a -> q) (a -> f) = Equiv q f
Equiv (Query s a) (m [b]) = (Res a ~ b, Backend m ~ s)
type CompResult = (Text, [Either Int Param], [SqlTypeRep])
class Preparable q where
-- | Prepare the query and parameter list.
mkQuery :: MonadSelda m
=> Int -- ^ Next argument index.
-> q -- ^ The query.
-> [SqlTypeRep] -- ^ The list of param types so far.
-> m CompResult
-- | Some parameterized query @q@ that can be prepared into a function @f@
-- in some @MonadSelda@.
class Prepare q f where
-- | Build the function that prepares and execute the query.
mkFun :: Preparable q
=> IORef (Maybe (BackendID, CompResult))
-> StmtID
-> q
-> [Param]
-> f
instance (SqlType a, Prepare q b) => Prepare q (a -> b) where
mkFun ref sid qry ps x = mkFun ref sid qry (param x : ps)
instance (Typeable a, MonadSelda m, a ~ Res (ResultT q), Result (ResultT q)) =>
Prepare q (m [a]) where
-- This function uses read/writeIORef instead of atomicModifyIORef.
-- For once, this is actually safe: the IORef points to a single compiled
-- statement, so the only consequence of a race between the read and the write
-- is that the statement gets compiled (note: NOT prepared) twice.
mkFun ref (StmtID sid) qry arguments = withConnection $ \conn -> do
let backend = connBackend conn
args = reverse arguments
stmts <- liftIO $ readIORef (connStmts conn)
case M.lookup sid stmts of
Just stm -> do
-- Statement already prepared for this connection; just execute it.
liftIO $ runQuery conn stm args
_ -> do
-- Statement wasn't prepared for this connection; check if it was at
-- least previously compiled for this backend.
compiled <- liftIO $ readIORef ref
(q, params, reps) <- case compiled of
Just (bid, comp) | bid == backendId backend -> do
return comp
_ -> do
comp <- mkQuery firstParamIx qry []
liftIO $ writeIORef ref (Just (backendId backend, comp))
return comp
-- Prepare and execute
liftIO $ mask $ \restore -> do
hdl <- prepareStmt backend (StmtID sid) reps q
let stm = SeldaStmt
{ stmtHandle = hdl
, stmtParams = params
, stmtText = q
}
atomicModifyIORef' (connStmts conn) $ \m -> (M.insert sid stm m, ())
restore $ runQuery conn stm args
where
runQuery conn stm args = do
let ps = replaceParams (stmtParams stm) args
hdl = stmtHandle stm
res <- runPrepared (connBackend conn) hdl ps
return $ map (buildResult (Proxy :: Proxy (ResultT q))) (snd res)
instance (SqlType a, Preparable b) => Preparable (Col s a -> b) where
mkQuery n f ts = mkQuery (n+1) (f x) (t : ts)
where
t = sqlType (Proxy :: Proxy a)
x = One $ Lit $ LCustom t (throw (Placeholder n) :: Lit a)
instance Result a => Preparable (Query s a) where
mkQuery _ q types = withBackend $ \b -> do
case compileWith (ppConfig b) q of
(q', ps) -> do
(ps', types') <- liftIO $ inspectParams (reverse types) ps
return (q', ps', types')
-- | Create a prepared Selda function. A prepared function has zero or more
-- arguments, and will get compiled into a prepared statement by the first
-- backend to execute it. Any subsequent calls to the function for the duration
-- of the connection to the database will reuse the prepared statement.
--
-- Preparable functions are of the form
-- @(SqlType a, SqlType b, ...) => Col s a -> Col s b -> ... -> Query s r@.
-- The resulting prepared function will be of the form
-- @MonadSelda m => a -> b -> ... -> m [Res r]@.
-- Note, however, that when using @prepared@, you must give a concrete type
-- for @m@ due to how Haskell's type class resolution works.
--
-- Prepared functions rely on memoization for just-in-time preparation and
-- caching. This means that if GHC accidentally inlines your prepared function,
-- it may get prepared twice.
-- While this does not affect the correctness of your program, and is
-- fairly unlikely to happen, if you want to be absolutely sure that your
-- queries aren't re-prepared more than absolutely necessary,
-- consider adding a @NOINLINE@ annotation to each prepared function.
--
-- Note that when using a constrained backend type variable (i.e.
-- @foo :: Bar b => SeldaM b [Int]@), optimizations must be enabled for
-- prepared statements to be effective.
--
-- A usage example:
--
-- > persons :: Table (Text, Int)
-- > (persons, name :*: age) = tableWithSelectors "ages" [name :- primary]
-- >
-- > {-# NOINLINE ageOf #-}
-- > ageOf :: Text -> SeldaM [Int]
-- > ageOf = prepared $ \n -> do
-- > person <- select ages
-- > restrict $ (person!name .== n)
-- > return age
{-# NOINLINE prepared #-}
prepared :: (Preparable q, Prepare q f, Equiv q f) => q -> f
prepared q = unsafePerformIO $ do
ref <- newIORef Nothing
sid <- freshStmtId
return $ mkFun ref sid q []
-- | Replace every indexed parameter with the corresponding provided parameter.
-- Keep all non-indexed parameters in place.
replaceParams :: [Either Int Param] -> [Param] -> [Param]
replaceParams params = map fromRight . go firstParamIx params
where
go n ps (x:xs) = go (n+1) (map (subst n x) ps) xs
go _ ps _ = ps
subst n x (Left n') | n == n' = Right x
subst _ _ old = old
fromRight (Right x) = x
fromRight _ = error "BUG: query parameter not substituted!"
-- | Inspect a list of parameters, denoting each parameter with either a
-- placeholder index or a literal parameter.
inspectParams :: [SqlTypeRep] -> [Param] -> IO ([Either Int Param], [SqlTypeRep])
inspectParams ts (x:xs) = do
res <- try $ pure $! forceParam x
let (x', t) = case res of
Right p -> (Right p, paramType p)
Left (Placeholder ix) -> (Left ix, ts !! ix)
(xs', ts') <- inspectParams ts xs
return (x' : xs', t : ts')
inspectParams _ [] = do
return ([], [])
-- | Force a parameter deep enough to determine whether it is a placeholder.
forceParam :: Param -> Param
forceParam p@(Param (LCustom _ x)) | x `seq` True = p
forceParam p = p