{-# 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 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