pg-store-0.2: src/Database/PostgreSQL/Store/Query/Builder.hs
{-# LANGUAGE OverloadedStrings, RecordWildCards, TypeSynonymInstances, FlexibleInstances #-}
-- |
-- Module: Database.PostgreSQL.Store.Query.Builder
-- Copyright: (c) Ole Krüger 2016
-- License: BSD3
-- Maintainer: Ole Krüger <ole@vprsm.de>
module Database.PostgreSQL.Store.Query.Builder (
-- * Query Builder
QueryBuilder,
insertCode,
insertTypedValue,
insertValue,
insertValue',
insertQuote,
insertName,
insertCommaSeperated,
-- * Generalized Building
FromQueryBuilder (..)
) where
import Control.Monad.State.Strict
import Data.List
import qualified Data.ByteString as B
import Database.PostgreSQL.LibPQ (invalidOid)
import Database.PostgreSQL.Store.Types
import Database.PostgreSQL.Store.Utilities
-- | Internal builder state
data BuilderState = BuilderState {
queryCode :: B.ByteString,
queryIndex :: Word,
queryValues :: [TypedValue]
}
-- | Query builder
type QueryBuilder = State BuilderState ()
-- | Insert a piece of SQL.
insertCode :: B.ByteString -> QueryBuilder
insertCode code =
modify (\ state -> state {queryCode = B.append (queryCode state) code})
-- | Insert a parameter placeholder into the code and attach the typed value to the query.
insertTypedValue :: TypedValue -> QueryBuilder
insertTypedValue typedValue =
modify $ \ BuilderState {..} ->
BuilderState {
queryCode = B.concat [queryCode, B.singleton 36, showByteString queryIndex],
queryIndex = queryIndex + 1,
queryValues = queryValues ++ [typedValue]
}
-- | Same as 'insertTypedValue' but untyped.
insertValue :: Value -> QueryBuilder
insertValue value =
insertTypedValue (TypedValue invalidOid (Just value))
-- | Extension of 'insertValue' which will add a type hint to the parameter placeholder.
insertValue' :: B.ByteString -> Value -> QueryBuilder
insertValue' typ value = do
insertCode "("
insertValue value
insertCode "::"
insertCode typ
insertCode ")"
-- | Insert a quote into the code.
insertQuote :: B.ByteString -> QueryBuilder
insertQuote contents =
insertCode (B.concat [B.singleton 39, -- '
B.concatMap replaceDelim contents,
B.singleton 39])
where
replaceDelim 39 = B.pack [39, 39]
replaceDelim x = B.singleton x
-- | Join several builders into a comma-seperated list.
insertCommaSeperated :: [QueryBuilder] -> QueryBuilder
insertCommaSeperated bs =
sequence_ (intersperse (insertCode ",") bs)
-- | Insert a name into the code. It will be surrounded by double quotes if necessary.
insertName :: B.ByteString -> QueryBuilder
insertName name =
if isAllowed then
insertCode name
else
insertCode (B.concat [B.singleton 34, -- "
B.intercalate (B.pack [34, 34]) (B.split 34 name),
B.singleton 34])
where
isAllowedHead b =
(b >= 97 && b <= 122) -- 'a' to 'z'
|| (b >= 65 && b <= 90) -- 'A' to 'Z'
|| b == 95 -- '_'
isAllowedBody b =
isAllowedHead b
|| (b >= 48 && b <= 57) -- '0' to '9'
isAllowed =
case B.uncons name of
Nothing -> True
Just (h, b) -> isAllowedHead h && B.all isAllowedBody b
-- | @a@ can be instantiated using the query builder.
class FromQueryBuilder a where
buildQuery :: QueryBuilder -> a
instance FromQueryBuilder QueryBuilder where
buildQuery = id
instance FromQueryBuilder B.ByteString where
buildQuery builder =
queryCode (execState builder (BuilderState B.empty 1 []))
instance FromQueryBuilder (B.ByteString, [TypedValue]) where
buildQuery builder =
(code, values)
where BuilderState code _ values = execState builder (BuilderState B.empty 1 [])
instance FromQueryBuilder (Query a) where
buildQuery builder =
Query code values
where BuilderState code _ values = execState builder (BuilderState B.empty 1 [])