module Database.PostgreSQL.Typed.Query
( PGQuery(..)
, PGSimpleQuery
, PGPreparedQuery
, rawPGSimpleQuery
, rawPGPreparedQuery
, QueryFlags(..)
, simpleFlags
, makePGQuery
, pgSQL
, pgExecute
, pgQuery
, pgLazyQuery
) where
import Control.Applicative ((<$>))
import Control.Arrow ((***), first, second)
import Control.Exception (try)
import Control.Monad (when, mapAndUnzipM)
import Data.Array (listArray, (!), inRange)
import Data.Char (isDigit, isSpace)
import Data.Foldable (toList)
import Data.List (dropWhileEnd)
import Data.Maybe (fromMaybe, isNothing)
import Data.Sequence (Seq)
import Data.Word (Word32)
import Language.Haskell.Meta.Parse (parseExp)
import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote (QuasiQuoter(..))
import Numeric (readDec)
import Database.PostgreSQL.Typed.Types
import Database.PostgreSQL.Typed.Protocol
import Database.PostgreSQL.Typed.TH
class PGQuery q a | q -> a where
pgRunQuery :: PGConnection -> q -> IO (Int, Seq a)
class PGQuery q PGValues => PGRawQuery q
pgExecute :: PGQuery q () => PGConnection -> q -> IO Int
pgExecute c q = fst <$> pgRunQuery c q
pgQuery :: PGQuery q a => PGConnection -> q -> IO [a]
pgQuery c q = toList . snd <$> pgRunQuery c q
data SimpleQuery = SimpleQuery String
instance PGQuery SimpleQuery PGValues where
pgRunQuery c (SimpleQuery sql) = pgSimpleQuery c sql
instance PGRawQuery SimpleQuery where
data PreparedQuery = PreparedQuery String [OID] PGValues [Bool]
instance PGQuery PreparedQuery PGValues where
pgRunQuery c (PreparedQuery sql types bind bc) = pgPreparedQuery c sql types bind bc
instance PGRawQuery PreparedQuery where
data QueryParser q a = QueryParser (PGTypeEnv -> q) (PGTypeEnv -> PGValues -> a)
instance PGRawQuery q => PGQuery (QueryParser q a) a where
pgRunQuery c (QueryParser q p) = second (fmap $ p e) <$> pgRunQuery c (q e) where e = pgTypeEnv c
instance Functor (QueryParser q) where
fmap f (QueryParser q p) = QueryParser q (\e -> f . p e)
rawParser :: q -> QueryParser q PGValues
rawParser q = QueryParser (const q) (const id)
type PGSimpleQuery = QueryParser SimpleQuery
type PGPreparedQuery = QueryParser PreparedQuery
rawPGSimpleQuery :: String -> PGSimpleQuery PGValues
rawPGSimpleQuery = rawParser . SimpleQuery
rawPGPreparedQuery :: String -> PGValues -> PGPreparedQuery PGValues
rawPGPreparedQuery sql bind = rawParser $ PreparedQuery sql [] bind []
pgLazyQuery :: PGConnection -> PGPreparedQuery a -> Word32
-> IO [a]
pgLazyQuery c (QueryParser q p) count =
fmap (p e) <$> pgPreparedLazyQuery c sql types bind bc count where
e = pgTypeEnv c
PreparedQuery sql types bind bc = q e
sqlPlaceholders :: String -> (String, [String])
sqlPlaceholders = sph (1 :: Int) where
sph n ('$':'$':'{':s) = first (('$':) . ('{':)) $ sph n s
sph n ('$':'{':s)
| (e, '}':r) <- break (\c -> c == '{' || c == '}') s =
(('$':show n) ++) *** (e :) $ sph (succ n) r
| otherwise = error $ "Error parsing SQL statement: could not find end of expression: ${" ++ s
sph n (c:s) = first (c:) $ sph n s
sph _ "" = ("", [])
sqlSubstitute :: String -> [TH.Exp] -> TH.Exp
sqlSubstitute sql exprl = se sql where
bnds = (1, length exprl)
exprs = listArray bnds exprl
expr n
| inRange bnds n = exprs ! n
| otherwise = error $ "SQL placeholder '$" ++ show n ++ "' out of range (not recognized by PostgreSQL); literal occurrences may need to be escaped with '$$'"
se = uncurry ((+$+) . stringL) . ss
ss ('$':'$':d:r) | isDigit d = first (('$':) . (d:)) $ ss r
ss ('$':s@(d:_)) | isDigit d, [(n, r)] <- readDec s = ("", expr n +$+ se r)
ss (c:r) = first (c:) $ ss r
ss "" = ("", stringL "")
stringL :: String -> TH.Exp
stringL = TH.LitE . TH.StringL
(+$+) :: TH.Exp -> TH.Exp -> TH.Exp
infixr 5 +$+
TH.LitE (TH.StringL "") +$+ e = e
e +$+ TH.LitE (TH.StringL "") = e
TH.LitE (TH.StringL l) +$+ TH.LitE (TH.StringL r) = stringL (l ++ r)
l +$+ r = TH.InfixE (Just l) (TH.VarE '(++)) (Just r)
splitCommas :: String -> [String]
splitCommas = spl where
spl [] = []
spl [c] = [[c]]
spl (',':s) = "":spl s
spl (c:s) = (c:h):t where h:t = spl s
trim :: String -> String
trim = dropWhileEnd isSpace . dropWhile isSpace
data QueryFlags = QueryFlags
{ flagNullable :: Bool
, flagPrepare :: Maybe [String]
}
simpleFlags :: QueryFlags
simpleFlags = QueryFlags False Nothing
makePGQuery :: QueryFlags -> String -> TH.ExpQ
makePGQuery QueryFlags{ flagNullable = nulls, flagPrepare = prep } sqle = do
(pt, rt) <- tpgDescribe sqlp (fromMaybe [] prep) (not nulls)
when (length pt < length exprs) $ fail "Not all expression placeholders were recognized by PostgreSQL; literal occurrences of '${' may need to be escaped with '$${'"
e <- TH.newName "tenv"
(vars, vals) <- mapAndUnzipM (\t -> do
v <- TH.newName $ 'p':tpgValueName t
return (TH.VarP v, tpgTypeEncoder (isNothing prep) t e v)) pt
(pats, conv, bc) <- unzip3 <$> mapM (\t -> do
v <- TH.newName $ 'c':tpgValueName t
return (TH.VarP v, tpgTypeDecoder t e v, tpgValueBinary t)) rt
let pgq
| isNothing prep = TH.ConE 'SimpleQuery `TH.AppE` sqlSubstitute sqlp vals
| otherwise = TH.ConE 'PreparedQuery `TH.AppE` stringL sqlp `TH.AppE` TH.ListE (map (TH.LitE . TH.IntegerL . toInteger . tpgValueTypeOID) pt) `TH.AppE` TH.ListE vals `TH.AppE` TH.ListE (map boolL bc)
foldl TH.AppE (TH.LamE vars $ TH.ConE 'QueryParser
`TH.AppE` TH.LamE [TH.VarP e] pgq
`TH.AppE` TH.LamE [TH.VarP e, TH.ListP pats] (TH.TupE conv))
<$> mapM parse exprs
where
(sqlp, exprs) = sqlPlaceholders sqle
parse e = either (fail . (++) ("Failed to parse expression {" ++ e ++ "}: ")) return $ parseExp e
boolL False = TH.ConE 'False
boolL True = TH.ConE 'True
qqQuery :: QueryFlags -> String -> TH.ExpQ
qqQuery f@QueryFlags{ flagNullable = False } ('?':q) = qqQuery f{ flagNullable = True } q
qqQuery f@QueryFlags{ flagPrepare = Nothing } ('$':q) = qqQuery f{ flagPrepare = Just [] } q
qqQuery f@QueryFlags{ flagPrepare = Just [] } ('(':s) = qqQuery f{ flagPrepare = Just args } =<< sql r where
args = map trim $ splitCommas arg
(arg, r) = break (')' ==) s
sql (')':q) = return q
sql _ = fail "pgSQL: unterminated argument list"
qqQuery f q = makePGQuery f q
qqTop :: Bool -> String -> TH.DecsQ
qqTop True ('!':sql) = qqTop False sql
qqTop err sql = do
r <- TH.runIO $ try $ withTPGConnection $ \c ->
pgSimpleQuery c sql
either ((if err then TH.reportError else TH.reportWarning) . (show :: PGError -> String)) (const $ return ()) r
return []
pgSQL :: QuasiQuoter
pgSQL = QuasiQuoter
{ quoteExp = qqQuery simpleFlags
, quoteType = const $ fail "pgSQL not supported in types"
, quotePat = const $ fail "pgSQL not supported in patterns"
, quoteDec = qqTop True
}