{-# LANGUAGE QuasiQuotes, TemplateHaskell, BangPatterns, GeneralizedNewtypeDeriving, MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances, FlexibleContexts, TypeFamilies #-} -- | -- Module: Database.PostgreSQL.Store.Query -- Copyright: (c) Ole Krüger 2015-2016 -- License: BSD3 -- Maintainer: Ole Krüger module Database.PostgreSQL.Store.Query ( -- * Query Query (..), SelectorElement (..), QueryTable (..), pgsq, pgss, -- * Helpers quoteIdentifier, -- * Query builder QueryCode, QueryBuildable, QueryBuilder, runQueryBuilder, writeCode, writeStringCode, writeIdentifier, writeAbsIdentifier, writeParam, writeColumn, intercalateBuilder ) where import Language.Haskell.TH import Language.Haskell.TH.Quote import Control.Applicative import Control.Monad.State.Strict import Data.List import Data.Proxy import Data.Monoid import Data.String import qualified Data.Text as T import qualified Data.ByteString as B import Data.Char import Data.Attoparsec.Text import Database.PostgreSQL.Store.Columns -- | Properly quote an identifier. quoteIdentifier :: String -> String quoteIdentifier name = '"' : stripQuotes name ++ "\"" where stripQuotes [] = [] stripQuotes ('"' : xs) = '"' : '"' : stripQuotes xs stripQuotes (x : xs) = x : stripQuotes xs -- | Query including statement and parameters -- -- Use the 'pgsq' quasi-quoter to conveniently create queries. data Query = Query { -- | Statement queryStatement :: !B.ByteString, -- | Parameters queryParams :: ![Value] } deriving (Show, Eq, Ord) -- | @SELECT@ expression data SelectorElement = SelectorField String -- ^ Select a field. The field nme will be quoted and properly escaped. | SelectorSpecial String -- ^ Select a special expression. The expression will be inlined as is. deriving (Show, Eq, Ord) -- | A type which implements this class can be used as a table in a quasi-quoted query. -- 'mkTable' can implement this for you. class QueryTable a where -- | Unquoted name of the table tableName :: Proxy a -> String -- | Unquoted name of the ID field tableIDName :: Proxy a -> String -- | Selectors needed to retrieve all fields necessary to construct the type - think @SELECT@. tableSelectors :: Proxy a -> [SelectorElement] -- | Generate the quoted identifier for a table. makeTableIdentifier :: (QueryTable a, IsString b) => Proxy a -> b makeTableIdentifier proxy = fromString (quoteIdentifier (tableName proxy)) -- | Generate the quoted identifier for table's id column. makeTableIDIdentifier :: (QueryTable a, IsString b) => Proxy a -> b makeTableIDIdentifier proxy = fromString (quoteIdentifier (tableName proxy) ++ "." ++ quoteIdentifier (tableIDName proxy)) -- | Generate the list of expression used as selector. makeTableSelectors :: (QueryTable a, IsString b) => Proxy a -> b makeTableSelectors proxy = fromString (intercalate ", " (map makeElement (tableSelectors proxy))) where makeElement (SelectorField name) = quoteIdentifier (tableName proxy) ++ "." ++ quoteIdentifier name makeElement (SelectorSpecial expr) = expr -- | Query segment data Segment = SSelector String | STable String | SVariable String | SIdentifier String | SQuote Char String | SOther String -- | Name name :: Parser String name = (:) <$> (letter <|> char '_') <*> many (satisfy isAlphaNum <|> char '_') -- | Type name typeName :: Parser String typeName = (:) <$> satisfy isUpper <*> many (satisfy isAlphaNum <|> char '_') -- | Qualified type name qualifiedTypeName :: Parser String qualifiedTypeName = do intercalate "." <$> sepBy1 typeName (char '.') -- | Quote quote :: Char -> Parser Segment quote delim = do char delim cnt <- concat <$> many (choice [escapedDelim, notDelim]) char delim pure (SQuote delim cnt) where escapedDelim = (\ a b -> [a, b]) <$> char '\\' <*> char delim notDelim = (: []) <$> notChar delim -- | Segments segments :: Parser [Segment] segments = many (choice [quote '"', quote '\'', char '#' >> SSelector <$> qualifiedTypeName, char '@' >> STable <$> qualifiedTypeName, char '&' >> SIdentifier <$> qualifiedTypeName, char '$' >> SVariable <$> name, SOther "#" <$ char '#', SOther "@" <$ char '@', SOther "&" <$ char '&', SOther "$" <$ char '$', SOther <$> some (satisfy (notInClass "\"'#@&$"))]) -- | Reduce segments in order to resolve names and collect query parameters. reduceSegment :: Segment -> QueryBuilder [Q Exp] (Q Exp) reduceSegment seg = case seg of SOther str -> writeStringCode str SQuote delim cnt -> writeStringCode (delim : cnt ++ [delim]) SVariable varName -> writeParam $ do mbName <- lookupValueName varName case mbName of Just name -> [e| pack $(varE name) |] Nothing -> fail ("'" ++ varName ++ "' does not refer to anything") STable tableName -> writeCode $ do mbName <- lookupTypeName tableName case mbName of Just table -> [e| makeTableIdentifier (Proxy :: Proxy $(conT table)) |] Nothing -> fail ("'" ++ tableName ++ "' does not refer to anything") SSelector tableName -> writeCode $ do mbName <- lookupTypeName tableName case mbName of Just table -> [e| makeTableSelectors (Proxy :: Proxy $(conT table)) |] Nothing -> fail ("'" ++ tableName ++ "' does not refer to anything") SIdentifier tableName -> writeCode $ do mbName <- lookupTypeName tableName case mbName of Just table -> [e| makeTableIDIdentifier (Proxy :: Proxy $(conT table)) |] Nothing -> fail ("'" ++ tableName ++ "' does not refer to anything") -- | Parse quasi-quoted query. parseStoreQueryE :: String -> Q Exp parseStoreQueryE code = do case parseOnly (segments <* endOfInput) (T.pack code) of Left msg -> fail msg Right xs -> do runQueryBuilder (mapM_ reduceSegment xs) -- | This quasi-quoter allows you to generate instances of 'Query'. It lets you write SQL with some -- small enhancements. 'pgsq' heavily relies on 'QueryTable' which can be implemented by 'mkTable' -- for a type of your choice. -- -- Some syntax definitions that might be useful later on: -- -- > TypeName ::= UpperAlpha {AlphaNumeric | '_'} -- > Name ::= (Alpha | '_') {AlphaNumeric | '_'} -- > QualifiedTypeName ::= {TypeName '.'} TypeName -- -- @Alpha@ includes all alphabetical characters; @UpperAlpha@ includes all upper-case alphabetical -- characters; @AlphaNumeric@ includes all alpha-numeric characters. -- -- = Embed values -- You can embed values whose types implement 'Column'. -- -- > ValueExp ::= '$' Name -- -- > magicNumber :: Int -- > magicNumber = 1337 -- > -- > myQuery :: Query -- > myQuery = -- > [pgsq| SELECT * FROM table t WHERE t.column1 > $magicNumber AND t.column2 < $otherNumber |] -- > where otherNumber = magicNumber * 2 -- -- @$magicNumber@ and @$otherNumber@ are references to values @magicNumber@ and @otherNumber@. -- -- The quasi-quoter will generate a 'Query' expression similar to the following. -- -- > Query "SELECT * FROM table t WHERE t.column1 > $1 AND t.column2 < $2" -- > [pack magicNumber, pack otherNumber] -- -- = Table names -- Types that implement 'QueryTable' associate a table name with themselves. Since the table name is -- not always known to the user, one can insert it dynamically. -- -- > TableNameExp ::= '@' QualifiedTypeName -- -- The @\@@-operators is also an alias for the function @ABS@. If you have an expression that -- triggers the quasi-quoter such as @\@A@, but you would like to use the @ABS@ functionality, then -- simply reformat your expression to @\@(A)@ or @\@\"A\"@ or @ABS(A)@. -- -- > instance QueryTable YourType where -- > tableName _ = "YourTable" -- > -- > myQuery :: Query -- > myQuery = -- > [pgsq| SELECT * FROM @Table WHERE @Table.column = 1337 |] -- -- The table name will be inlined which results in the following. -- -- > Query "SELECT * FROM \"YourTable\" WHERE \"YourTable\".column = 1337" [] -- -- = Identifier column names -- Each instance of 'QueryTable' also provides the name of the identifier column. Using this column -- name you can identify specific rows of a certain table. -- -- > TableIdentExp ::= '&' TypeName -- -- @&@ is also the operator for bitwise-AND. To resolve the ambiguity for expressions like @A&B@, -- simply reformat it to @A & B@ or @A&(B)@ or @A&\"B\"@. -- -- > instance QueryTable YourType where -- > tableName _ = "YourTable" -- > tableIDName _ = "id" -- > -- > listIDs :: Query -- > listIDs = -- > [pgsq| SELECT &YourType FROM @YourType |] -- -- @listIDs@ is now a query which lists the IDs of each row. This is especially useful in -- combination with 'Reference'. -- -- > fetchIDs :: Errand [Reference YourType] -- > fetchIDs = -- > query [pgsq| SELECT &YourType FROM @YourType |] -- -- Note, just like @\@@, the operator @&@ is reserved. Although @A&B@ is a valid SQL expression, it -- will trigger the quasi-quoter. To avoid this, you reform your expression to @A & B@ or @A&(B)@. -- -- = Selectors -- 'mkTable' will automatically implement 'Result' and 'QueryTable' for you. This allows you to make -- use of the selector expander. -- -- > SelectorExp ::= '#' QualifiedTypeName -- -- @#@ is also the operator for bitwise-XOR. To resolve the ambiguity for expressions like @A#B@, -- simply reformat it to @A # B@ or @A#(B)@ or @A#\"B\"@. -- -- > data Actor = Actor { -- > actorName :: String, -- > actorAge :: Word -- > } deriving (Show, Eq, Ord) -- > -- > mkTable ''Actor [] -- > -- > fetchOldActors :: Errand [Actor] -- > fetchOldActors = -- > query [pgsq| SELECT #Actor FROM @Actor a WHERE a.actorAge >= $oldAge |] -- > where oldAge = 70 -- -- @#Actor@ will expand to a list of columns that are necessary to construct an instance of @Actor@. -- In this case it is equivalent to -- -- > @Actor.actorName, @Actor.actorAge pgsq :: QuasiQuoter pgsq = QuasiQuoter { quoteExp = parseStoreQueryE, quotePat = const (fail "Cannot use 'pgsq' in pattern"), quoteType = const (fail "Cannot use 'pgsq' in type"), quoteDec = const (fail "Cannot use 'pgsq' in declaration") } -- | Parse quasi-quoted query but return only statement. parseStoreStatementE :: String -> Q Exp parseStoreStatementE code = do case parseOnly (segments <* endOfInput) (T.pack code) of Left msg -> fail (show msg) Right xs -> do [e| mconcat $(listE (runQueryBuilder_ (mapM_ reduceSegment xs))) |] -- | Just like 'pgsq' but only produces the statement associated with the query. Referenced -- values are not inlined, they are simply dismissed. pgss :: QuasiQuoter pgss = QuasiQuoter { quoteExp = parseStoreStatementE, quotePat = const (fail "Cannot use 'pgss' in pattern"), quoteType = const (fail "Cannot use 'pgss' in type"), quoteDec = const (fail "Cannot use 'pgss' in declaration") } -- | Internal state for every builder data BuilderState s p = BuilderState s Word [p] -- | class QueryCode s where type Code s appendCode :: s -> Code s -> s appendStringCode :: s -> String -> s instance QueryCode B.ByteString where type Code B.ByteString = B.ByteString appendCode = B.append appendStringCode bs str = bs <> fromString str instance QueryCode [Q Exp] where type Code [Q Exp] = Q Exp appendCode segments exp = segments ++ [exp] appendStringCode segments str = appendCode segments [e| fromString $(stringE str) |] instance QueryCode String where type Code String = String appendCode segments code = segments ++ code appendStringCode = appendCode -- | Can build @o@ using @s@ and @[p]@. class QueryBuildable s p o | s p -> o where buildQuery :: s -> [p] -> o instance QueryBuildable B.ByteString Value Query where buildQuery = Query instance QueryBuildable String (Q Exp) (Q Exp) where buildQuery code params = [e| Query (fromString $(stringE code)) $(listE params) |] instance QueryBuildable [Q Exp] (Q Exp) (Q Exp) where buildQuery codeSegments params = [e| Query (B.concat $(listE codeSegments)) $(listE params) |] -- | Query builder type QueryBuilder s p = State (BuilderState s p) () -- | Run query builder. runQueryBuilder :: (QueryBuildable s p o, Monoid s) => QueryBuilder s p -> o runQueryBuilder builder = buildQuery code params where BuilderState code _ params = execState builder (BuilderState mempty 1 []) -- | Run query builder, return only the statement. runQueryBuilder_ :: (Monoid s) => QueryBuilder s p -> s runQueryBuilder_ builder = code where BuilderState code _ _ = execState builder (BuilderState mempty 1 []) -- | Write code. writeCode :: (QueryCode s) => Code s -> QueryBuilder s p writeCode code = modify $ \ (BuilderState stmt idx params) -> BuilderState (appendCode stmt code) idx params -- | Write string code. writeStringCode :: (QueryCode s) => String -> QueryBuilder s p writeStringCode code = modify $ \ (BuilderState stmt idx params) -> BuilderState (appendStringCode stmt code) idx params -- | Add an identifier. writeIdentifier :: (QueryCode s) => String -> QueryBuilder s p writeIdentifier name = writeStringCode (quoteIdentifier name) -- | Add an absolute identifier. writeAbsIdentifier :: (QueryCode s) => String -> String -> QueryBuilder s p writeAbsIdentifier ns name = do writeIdentifier ns writeStringCode "." writeIdentifier name -- | Embed a parameter. writeParam :: (QueryCode s) => p -> QueryBuilder s p writeParam param = do modify $ \ (BuilderState code idx params) -> BuilderState (appendStringCode code ("$" ++ show idx)) (idx + 1) (params ++ [param]) -- | Embed a value parameter. writeColumn :: (Column p, QueryCode s) => p -> QueryBuilder s Value writeColumn param = writeParam (pack param) -- | Do something between other builders. intercalateBuilder :: QueryBuilder s p -> [QueryBuilder s p] -> QueryBuilder s p intercalateBuilder x xs = sequence_ (intersperse x xs)