pg-store-0.2: src/Database/PostgreSQL/Store/Entity.hs
{-# LANGUAGE OverloadedStrings,
ConstraintKinds,
DataKinds,
DefaultSignatures,
FlexibleContexts,
FlexibleInstances,
ScopedTypeVariables,
TypeFamilies,
TypeOperators,
TypeSynonymInstances,
UndecidableInstances
#-}
-- |
-- Module: Database.PostgreSQL.Store.Entity
-- Copyright: (c) Ole Krüger 2016
-- License: BSD3
-- Maintainer: Ole Krüger <ole@vprsm.de>
module Database.PostgreSQL.Store.Entity (
-- * Result and query entity
Entity (..),
insertGeneric,
parseGeneric,
-- * Helpers
GEntityRecord (..),
GEntityEnum (..),
GEntity (..)
) where
import GHC.Generics
import GHC.TypeLits
import Control.Applicative
import Control.Monad
import Data.Int
import Data.Bits
import Data.Word
import Data.Scientific hiding (scientific)
import Numeric
import Numeric.Natural
import Data.Bifunctor
import Data.Proxy
import qualified Data.Aeson as A
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import Data.Attoparsec.ByteString
import Data.Attoparsec.ByteString.Char8 (signed, decimal, skipSpace, double, scientific)
import Database.PostgreSQL.Store.Types
import Database.PostgreSQL.Store.Utilities
import Database.PostgreSQL.Store.Generics
import Database.PostgreSQL.Store.Query.Builder
import Database.PostgreSQL.Store.RowParser
import Database.PostgreSQL.LibPQ (Oid (..), invalidOid)
-- | Generic record entity
class GEntityRecord (rec :: KRecord) where
gInsertRecord :: Record rec -> QueryBuilder
gParseRecord :: RowParser (Record rec)
instance (Entity typ) => GEntityRecord ('TSingle meta typ) where
gInsertRecord (Single x) =
insertEntity x
gParseRecord =
Single <$> parseEntity
instance (GEntityRecord lhs, GEntityRecord rhs) => GEntityRecord ('TCombine lhs rhs) where
gInsertRecord (Combine lhs rhs) = do
gInsertRecord lhs
insertCode ","
gInsertRecord rhs
gParseRecord =
Combine <$> gParseRecord <*> gParseRecord
-- | Generic enumeration entity
class GEntityEnum (enum :: KFlatSum) where
gInsertEnum :: FlatSum enum -> QueryBuilder
gEnumValues :: [(B.ByteString, FlatSum enum)]
instance (KnownSymbol name) => GEntityEnum ('TValue ('MetaCons name f r)) where
gInsertEnum _ =
insertQuote (buildByteString (symbolVal (Proxy :: Proxy name)))
gEnumValues =
[(buildByteString (symbolVal (Proxy :: Proxy name)), Unit)]
instance (GEntityEnum lhs, GEntityEnum rhs) => GEntityEnum ('TChoose lhs rhs) where
gInsertEnum (ChooseLeft lhs) = gInsertEnum lhs
gInsertEnum (ChooseRight rhs) = gInsertEnum rhs
gEnumValues =
map (second ChooseLeft) gEnumValues
++ map (second ChooseRight) gEnumValues
-- | Generic entity
class GEntity (dat :: KDataType) where
gInsertEntity :: DataType dat -> QueryBuilder
gParseEntity :: RowParser (DataType dat)
instance (GEntityRecord rec) => GEntity ('TRecord d c rec) where
gInsertEntity (Record x) =
gInsertRecord x
gParseEntity =
Record <$> gParseRecord
instance (GEntityEnum enum) => GEntity ('TFlatSum d enum) where
gInsertEntity (FlatSum x) =
gInsertEnum x
gParseEntity =
FlatSum <$> parseContents (`lookup` gEnumValues)
-- | Insert generic entity into the query.
insertGeneric :: (GenericEntity a, GEntity (AnalyzeEntity a)) => a -> QueryBuilder
insertGeneric x =
gInsertEntity (fromGenericEntity x)
-- | Generic 'RowParser' for an entity.
parseGeneric :: (GenericEntity a, GEntity (AnalyzeEntity a)) => RowParser a
parseGeneric =
toGenericEntity <$> gParseEntity
-- | An entity that is used as a parameter or result of a query.
class Entity a where
-- | Insert an instance of @a@ into the query.
insertEntity :: a -> QueryBuilder
default insertEntity :: (GenericEntity a, GEntity (AnalyzeEntity a)) => a -> QueryBuilder
insertEntity = insertGeneric
-- | Retrieve an instance of @a@ from the result set.
parseEntity :: RowParser a
default parseEntity :: (GenericEntity a, GEntity (AnalyzeEntity a)) => RowParser a
parseEntity = parseGeneric
-- | 2 result entities in sequence
instance (Entity a, Entity b) => Entity (a, b)
-- | 3 result entities in sequence
instance (Entity a, Entity b, Entity c) => Entity (a, b, c)
-- | 4 result entities in sequence
instance (Entity a, Entity b, Entity c, Entity d) => Entity (a, b, c, d)
-- | 5 result entities in sequence
instance (Entity a, Entity b, Entity c, Entity d, Entity e) => Entity (a, b, c, d, e)
-- | 6 result entities in sequence
instance (Entity a, Entity b, Entity c, Entity d, Entity e, Entity f) => Entity (a, b, c, d, e, f)
-- | 7 result entities in sequence
instance (Entity a, Entity b, Entity c, Entity d, Entity e, Entity f, Entity g) => Entity (a, b, c, d, e, f, g)
-- | 'QueryBuilder'
instance Entity QueryBuilder where
insertEntity = id
parseEntity = do
colsLeft <- columnsLeft
insertCommaSeperated <$> replicateM (fromEnum colsLeft) (insertTypedValue <$> fetchColumn)
-- | Untyped column value
instance Entity Value where
insertEntity = insertValue
parseEntity = parseColumn (\ (TypedValue _ mbValue) -> mbValue)
-- | Typed column value
instance Entity TypedValue where
insertEntity = insertTypedValue
parseEntity = fetchColumn
-- | A value which may normally not be @NULL@.
instance (Entity a) => Entity (Maybe a) where
insertEntity Nothing = insertTypedValue (TypedValue invalidOid Nothing)
insertEntity (Just x) = insertEntity x
parseEntity = do
TypedValue _ value <- peekColumn
case value of
Nothing -> pure Nothing
_ -> Just <$> parseEntity
-- | @boolean@
instance Entity Bool where
insertEntity input =
insertTypedValue (TypedValue (Oid 16) (Just (Value value)))
where value | input = "t" | otherwise = "f"
parseEntity =
parseContents $ \ dat ->
Just (elem dat ["t", "1", "true", "TRUE", "y", "yes", "YES", "on", "ON"])
-- | Parse a column using the given 'Parser'.
parseContentsWith :: Parser a -> RowParser a
parseContentsWith p =
parseContents (maybeResult . endResult . parse p)
where
endResult (Partial f) = f B.empty
endResult x = x
-- | Simplified version of 'insertTypedValue'
insertTypedValue_ :: Oid -> B.ByteString -> QueryBuilder
insertTypedValue_ typ val =
insertTypedValue (TypedValue typ (Just (Value val)))
-- | Insert a numeric value.
insertNumericValue :: (Show a) => a -> QueryBuilder
insertNumericValue x =
insertTypedValue_ (Oid 1700) (showByteString x)
-- | Any integer
instance Entity Integer where
insertEntity = insertNumericValue
parseEntity = parseContentsWith (signed decimal)
-- | Any integer
instance Entity Int where
insertEntity = insertNumericValue
parseEntity = parseContentsWith (signed decimal)
-- | Any integer
instance Entity Int8 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith (signed decimal)
-- | Any integer
instance Entity Int16 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith (signed decimal)
-- | Any integer
instance Entity Int32 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith (signed decimal)
-- | Any integer
instance Entity Int64 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith (signed decimal)
-- | Any unsigned integer
instance Entity Natural where
insertEntity = insertNumericValue
parseEntity = parseContentsWith decimal
-- | Any unsigned integer
instance Entity Word where
insertEntity = insertNumericValue
parseEntity = parseContentsWith decimal
-- | Any unsigned integer
instance Entity Word8 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith decimal
-- | Any unsigned integer
instance Entity Word16 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith decimal
-- | Any unsigned integer
instance Entity Word32 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith decimal
-- | Any unsigned integer
instance Entity Word64 where
insertEntity = insertNumericValue
parseEntity = parseContentsWith decimal
-- | Any floating-point number
instance Entity Double where
insertEntity = insertNumericValue
parseEntity = parseContentsWith double
-- | Any floating-point number
instance Entity Float where
insertEntity = insertNumericValue
parseEntity = (realToFrac :: Double -> Float) <$> parseEntity
-- | Any numeric type
instance Entity Scientific where
insertEntity x =
insertTypedValue_ (Oid 1700) (buildByteString (formatScientific Fixed Nothing x))
parseEntity = parseContentsWith scientific
-- | @char@, @varchar@ or @text@ - UTF-8 encoded
instance Entity String where
insertEntity value =
insertTypedValue_ (Oid 25) (buildByteString value)
parseEntity = T.unpack <$> parseEntity
-- | @char@, @varchar@ or @text@ - UTF-8 encoded
instance Entity T.Text where
insertEntity value =
insertTypedValue_ (Oid 25) (T.encodeUtf8 value)
parseEntity =
parseContents (either (const Nothing) Just . T.decodeUtf8')
-- | @char@, @varchar@ or @text@ - UTF-8 encoded
instance Entity TL.Text where
insertEntity value =
insertEntity (TL.toStrict value)
parseEntity =
parseContents (either (const Nothing) Just . TL.decodeUtf8' . BL.fromStrict)
-- | @bytea@ - byte array encoded in hex format
instance Entity B.ByteString where
insertEntity value =
insertTypedValue_ (Oid 17) dat
where
dat = B.append "\\x" (B.concatMap showHex' value)
showHex' n =
buildByteString $ case showHex n [] of
(a : b : _) -> [a, b]
(a : _) -> ['0', a]
[] -> "00"
parseEntity =
parseContentsWith (hexFormat <|> escapedFormat)
where
isHexChar x =
(x >= 48 && x <= 57) -- 0 - 9
|| (x >= 65 && x <= 70) -- A - Z
|| (x >= 97 && x <= 102) -- a - z
hexCharToWord x
| x >= 48 && x <= 57 = x - 48
| x >= 65 && x <= 70 = x - 55
| x >= 97 && x <= 102 = x - 87
| otherwise = 0
hexWord = do
skipSpace
a <- satisfy isHexChar
b <- satisfy isHexChar
pure (shiftL (hexCharToWord a) 4 .|. hexCharToWord b)
hexFormat = do
word8 92 -- \
word8 120 -- x
B.pack <$> many hexWord <* skipSpace
isOctChar x = x >= 48 && x <= 55
octCharToWord x
| isOctChar x = x - 48
| otherwise = 0
escapedWord = do
word8 92
a <- satisfy isOctChar
b <- satisfy isOctChar
c <- satisfy isOctChar
pure (shiftL (octCharToWord a) 6 .|. shiftL (octCharToWord b) 3 .|. c)
escapedBackslash = do
word8 92
word8 92
escapedFormat =
B.pack <$> many (escapedBackslash <|> escapedWord <|> anyWord8)
-- | @bytea@ - byte array encoded in hex format
instance Entity BL.ByteString where
insertEntity value =
insertEntity (BL.toStrict value)
parseEntity = BL.fromStrict <$> parseEntity
-- | @json@ or @jsonb@
instance Entity A.Value where
insertEntity value =
insertTypedValue_ (Oid 114) (BL.toStrict (A.encode value))
parseEntity = parseContents A.decodeStrict