module Database.PostgreSQL.Store.Query.TH (
pgQueryGen,
pgQuery,
pgPrepQuery
) where
import Language.Haskell.Meta.Parse
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Control.Applicative
import qualified Blaze.ByteString.Builder as B
import qualified Blaze.ByteString.Builder.Char.Utf8 as B
import Data.Attoparsec.Text
import qualified Data.ByteString as B
import Data.Char
import Data.List
import Data.Tagged
import qualified Data.Text as T
import Database.PostgreSQL.Store.Entity
import Database.PostgreSQL.Store.Query.Builder
import Database.PostgreSQL.Store.Table
import Database.PostgreSQL.Store.Tuple
import Database.PostgreSQL.Store.Utilities
valueName :: Parser String
valueName =
(:) <$> (letter <|> char '_') <*> many (satisfy isAlphaNum <|> char '_' <|> char '\'')
typeName :: Parser String
typeName =
(:) <$> satisfy isUpper <*> many (satisfy isAlphaNum <|> char '_' <|> char '\'')
qualifiedTypeName :: Parser String
qualifiedTypeName =
intercalate "." <$> sepBy1 typeName (char '.')
data QuerySegment
= QueryEntity String
| QueryEntityCode String
| QueryQuote Char String
| QueryOther String
| QueryTable String
| QuerySelector String
| QuerySelectorAlias String String
| QueryParam Word
deriving (Show, Eq, Ord)
tableSegment :: Parser QuerySegment
tableSegment = do
char '@'
QueryTable <$> qualifiedTypeName
selectorSegment :: Parser QuerySegment
selectorSegment = do
char '#'
QuerySelector <$> qualifiedTypeName
selectorAliasSegment :: Parser QuerySegment
selectorAliasSegment = do
char '#'
QuerySelectorAlias <$> qualifiedTypeName
<* char '('
<*> valueName
<* char ')'
paramSegment :: Parser QuerySegment
paramSegment = do
char '$'
QueryParam <$> decimal
entityNameSegment :: Parser QuerySegment
entityNameSegment = do
char '$'
QueryEntity <$> valueName
entityCodeSegment :: Parser QuerySegment
entityCodeSegment =
QueryEntityCode <$> (string "$(" *> insideCode <* char ')')
where
insideCode =
concat <$> many (choice [bracedCode,
quoteCode '\'',
quoteCode '\"',
some (satisfy (notInClass "\"'()"))])
bracedCode =
char '(' *> fmap (\ code -> '(' : code ++ ")") insideCode <* char ')'
quoteCode delim = do
char delim
cnt <- many (choice [escapedDelim delim, notDelim delim])
char delim
pure (delim : concat cnt ++ [delim])
escapedDelim delim = do
char '\\'
char delim
pure ['\\', delim]
notDelim delim =
(: []) <$> notChar delim
quoteSegment :: Char -> Parser QuerySegment
quoteSegment delim = do
char delim
cnt <- concat <$> many (choice [escapedDelim, notDelim])
char delim
pure (QueryQuote delim cnt)
where
escapedDelim = char delim >> char delim >> pure [delim, delim]
notDelim = (: []) <$> notChar delim
otherSegment :: Parser QuerySegment
otherSegment =
QueryOther <$> some (satisfy (notInClass "\"'@#$"))
querySegment :: Parser QuerySegment
querySegment =
choice [quoteSegment '\'',
quoteSegment '"',
tableSegment,
selectorAliasSegment,
selectorSegment,
paramSegment,
entityCodeSegment,
entityNameSegment,
otherSegment]
packCode :: String -> B.ByteString
packCode code =
B.toByteString (B.fromString code)
type TableTag t = Tagged t Table
tableDescriptionE :: Name -> Q Exp
tableDescriptionE typ =
[e| untag (describeTableType :: TableTag $(conT typ)) |]
translateSegment :: QuerySegment -> Q Exp
translateSegment segment =
case segment of
QueryTable stringName -> do
mbTypeName <- lookupTypeName stringName
case mbTypeName of
Nothing ->
fail ("'" ++ stringName ++ "' does not refer to a type")
Just typ ->
[e| genTableName $(tableDescriptionE typ) |]
QuerySelector stringName -> do
mbTypeName <- lookupTypeName stringName
case mbTypeName of
Nothing ->
fail ("'" ++ stringName ++ "' does not refer to a type")
Just typ ->
[e| genTableColumns $(tableDescriptionE typ) |]
QuerySelectorAlias stringName aliasName -> do
mbTypeName <- lookupTypeName stringName
case mbTypeName of
Nothing ->
fail ("'" ++ stringName ++ "' does not refer to a type")
Just typ ->
[e| genTableColumnsOn $(tableDescriptionE typ)
$(liftByteString (buildByteString aliasName)) |]
QueryEntity stringName -> do
mbValueName <- lookupValueName stringName
case mbValueName of
Nothing ->
fail ("'" ++ stringName ++ "' does not refer to a value")
Just name ->
[e| embedEntity $(varE name) |]
QueryEntityCode code ->
case parseExp code of
Left msg -> fail ("Error in code " ++ show code ++ ": " ++ msg)
Right expr -> pure expr
QueryQuote delim code ->
[e| Code $(liftByteString (packCode (delim : code ++ [delim]))) |]
QueryOther code ->
[e| Code $(liftByteString (packCode code)) |]
QueryParam idx -> do
let accessor =
case idx of
0 -> [e| getElement0 |]
1 -> [e| getElement1 |]
2 -> [e| getElement2 |]
3 -> [e| getElement3 |]
4 -> [e| getElement4 |]
5 -> [e| getElement5 |]
6 -> [e| getElement6 |]
7 -> [e| getElement7 |]
8 -> [e| getElement8 |]
9 -> [e| getElement9 |]
_ -> fail "Cannot use more than 10 parameters with the $n short cut"
[e| With $accessor genEntity |]
queryGenE :: String -> Q Exp
queryGenE code =
case parseOnly (many querySegment <* endOfInput) (T.strip (T.pack code)) of
Left msg ->
fail ("Query parser failed: " ++ msg)
Right [] ->
[e| mempty |]
Right segments ->
[e| mconcat $(ListE <$> mapM translateSegment segments) |]
pgQueryGen :: QuasiQuoter
pgQueryGen =
QuasiQuoter {
quoteExp = queryGenE,
quotePat = const (fail "Cannot use 'pgQueryGen' in pattern"),
quoteType = const (fail "Cannot use 'pgQueryGen' in type"),
quoteDec = const (fail "Cannot use 'pgQueryGen' in declaration")
}
queryE :: String -> Q Exp
queryE code =
[e| assemble $(queryGenE code) () |]
pgQuery :: QuasiQuoter
pgQuery =
QuasiQuoter {
quoteExp = queryE,
quotePat = const (fail "Cannot use 'pgQuery' in pattern"),
quoteType = const (fail "Cannot use 'pgQuery' in type"),
quoteDec = const (fail "Cannot use 'pgQuery' in declaration")
}
prepQueryE :: String -> Q Exp
prepQueryE code = do
Loc _ p m _ _ <- location
withPrefix (B.concat [buildByteString p, "_", buildByteString m, "_"])
where
withPrefix prefix =
[e| assemblePrep $(liftByteString prefix) $(queryGenE code) |]
pgPrepQuery :: QuasiQuoter
pgPrepQuery =
QuasiQuoter {
quoteExp = prepQueryE,
quotePat = const (fail "Cannot use 'pgPrepQuery' in pattern"),
quoteType = const (fail "Cannot use 'pgPrepQuery' in type"),
quoteDec = const (fail "Cannot use 'pgPrepQuery' in declaration")
}