{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE 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 (..), embedEntity, param0, param1, param2, param3, param4, param5, param6, param7, param8, param9, genGeneric, parseGeneric, -- * Helpers GEntityRecord (..), GEntity (..), GenericEntity ) where import GHC.Generics (Meta (..)) import GHC.TypeLits hiding (Text) import Control.Applicative import qualified Data.Aeson as A import Data.Attoparsec.ByteString import Data.Attoparsec.ByteString.Char8 (decimal, double, scientific, signed, skipSpace) import Data.Bits import qualified Data.ByteString as B import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL import Data.Int import Data.Proxy import Data.Scientific (FPFormat (Fixed), Scientific, formatScientific) import Data.Semigroup import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as TL import Data.Word import Numeric.Natural import Database.PostgreSQL.Store.Generics import Database.PostgreSQL.Store.Query.Builder import Database.PostgreSQL.Store.RowParser import Database.PostgreSQL.Store.Tuple import Database.PostgreSQL.Store.Types import Database.PostgreSQL.Store.Utilities -- | Generic record entity class (KnownNat (GRecordWidth rec)) => GEntityRecord (rec :: KRecord) where type GRecordWidth rec :: Nat gEmbedRecord :: QueryGenerator (Record rec) gParseRecord :: RowParser (GRecordWidth rec) (Record rec) instance (Entity typ) => GEntityRecord ('TSingle meta typ) where type GRecordWidth ('TSingle meta typ) = Width typ gEmbedRecord = With (\ (Single x) -> x) genEntity gParseRecord = Single <$> parseEntity instance (GEntityRecord lhs, GEntityRecord rhs, KnownNat (GRecordWidth lhs + GRecordWidth rhs)) => GEntityRecord ('TCombine lhs rhs) where type GRecordWidth ('TCombine lhs rhs) = GRecordWidth lhs + GRecordWidth rhs gEmbedRecord = mconcat [With (\ (Combine lhs _) -> lhs) gEmbedRecord, Code ",", With (\ (Combine _ rhs) -> rhs) gEmbedRecord] gParseRecord = Combine <$> gParseRecord <*>$ gParseRecord -- | Generic enumeration value class GEntityEnum (enum :: KFlatSum) where gEnumToPayload :: FlatSum enum -> B.ByteString gEnumFromPayload :: B.ByteString -> Maybe (FlatSum enum) instance (KnownSymbol name) => GEntityEnum ('TValue ('MetaCons name f r)) where gEnumToPayload _ = buildByteString (symbolVal @name Proxy) gEnumFromPayload value | value == buildByteString (symbolVal @name Proxy) = Just Unit | otherwise = Nothing instance (GEntityEnum lhs, GEntityEnum rhs) => GEntityEnum ('TChoose lhs rhs) where gEnumToPayload (ChooseLeft lhs) = gEnumToPayload lhs gEnumToPayload (ChooseRight rhs) = gEnumToPayload rhs gEnumFromPayload input = (ChooseLeft <$> gEnumFromPayload input) <|> (ChooseRight <$> gEnumFromPayload input) -- | Generic entity class (KnownNat (GEntityWidth dat)) => GEntity (dat :: KDataType) where type GEntityWidth dat :: Nat gEmbedEntity :: QueryGenerator (DataType dat) gParseEntity :: RowParser (GEntityWidth dat) (DataType dat) instance (GEntityRecord rec) => GEntity ('TRecord d c rec) where type GEntityWidth ('TRecord d c rec) = GRecordWidth rec gEmbedEntity = With (\ (Record x) -> x) gEmbedRecord gParseEntity = Record <$> gParseRecord instance (GEntityEnum enum) => GEntity ('TFlatSum d enum) where type GEntityWidth ('TFlatSum d enum) = 1 gEmbedEntity = Gen (Oid 0) (\ (FlatSum x) -> Just (gEnumToPayload x)) gParseEntity = retrieveContent >>=$ \ input -> case gEnumFromPayload input of Just x -> finish (FlatSum x) Nothing -> cancel ColumnRejected -- | This is required if you want to use the default implementations of 'genEntity'or 'parseEntity' -- with polymorphic data types. type GenericEntity a = (Generic a, GEntity (Rep a)) -- | Generic 'QueryGenerator' for an entity. genGeneric :: (Generic a, GEntity (Rep a)) => QueryGenerator a genGeneric = With fromGeneric gEmbedEntity -- | Generic 'RowParser' for an entity. parseGeneric :: (Generic a, GEntity (Rep a)) => RowParser (GEntityWidth (Rep a)) a parseGeneric = toGeneric <$> gParseEntity -- | An entity that is used as a parameter or result of a query. class (KnownNat (Width a)) => Entity a where -- | Number of values of which the entity consists type Width a :: Nat type Width a = GEntityWidth (Rep a) -- | Embed the entity into the query. genEntity :: QueryGenerator a default genEntity :: (Generic a, GEntity (Rep a)) => QueryGenerator a genEntity = genGeneric -- | Retrieve an instance of @a@ from the result set. parseEntity :: RowParser (Width a) a default parseEntity :: (Generic a, GEntity (Rep a), Width a ~ GEntityWidth (Rep a)) => RowParser (Width a) a parseEntity = parseGeneric -- | Embed an entity into the query. embedEntity :: (Entity e) => e -> QueryGenerator a embedEntity e = withOther e genEntity -- | Parameter entity at index 0 param0 :: (Entity r) => QueryGenerator (Tuple (r ': ts)) param0 = withParam0 genEntity -- | Parameter entity at index 1 param1 :: (Entity r) => QueryGenerator (Tuple (t0 ': r ': ts)) param1 = withParam1 genEntity -- | Parameter entity at index 2 param2 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': r ': ts)) param2 = withParam2 genEntity -- | Parameter entity at index 3 param3 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': r ': ts)) param3 = withParam3 genEntity -- | Parameter entity at index 4 param4 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': t3 ': r ': ts)) param4 = withParam4 genEntity -- | Parameter entity at index 5 param5 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': t3 ': t4 ': r ': ts)) param5 = withParam5 genEntity -- | Parameter entity at index 6 param6 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': t3 ': t4 ': t5 ': r ': ts)) param6 = withParam6 genEntity -- | Parameter entity at index 7 param7 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': t3 ': t4 ': t5 ': t6 ': r ': ts)) param7 = withParam7 genEntity -- | Parameter entity at index 8 param8 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': t3 ': t4 ': t5 ': t6 ': t7 ': r ': ts)) param8 = withParam8 genEntity -- | Parameter entity at index 9 param9 :: (Entity r) => QueryGenerator (Tuple (t0 ': t1 ': t2 ': t3 ': t4 ': t5 ': t6 ': t7 ': t8 ': r ': ts)) param9 = withParam9 genEntity -- | Chain of 2 entities instance (GenericEntity (a, b)) => Entity (a, b) -- | Chain of 3 entities instance (GenericEntity (a, b, c)) => Entity (a, b, c) -- | Chain of 4 entities instance (GenericEntity (a, b, c, d)) => Entity (a, b, c, d) -- | Chain of 5 entities instance (GenericEntity (a, b, c, d, e)) => Entity (a, b, c, d, e) -- | Chain of 6 entities instance (GenericEntity (a, b, c, d, e, f)) => Entity (a, b, c, d, e, f) -- | Chain of 7 entities instance (GenericEntity (a, b, c, d, e, f, g)) => Entity (a, b, c, d, e, f, g) -- | A value which may be @NULL@. instance (Entity a) => Entity (Maybe a) where type Width (Maybe a) = Width a genEntity = walkTree genEntity where walkTree :: QueryGenerator b -> QueryGenerator (Maybe b) walkTree (Gen oid f) = Gen oid (>>= f) walkTree (Code code) = Code code walkTree (With f gen) = With (fmap f) (walkTree gen) walkTree (Merge l r) = Merge (walkTree l) (walkTree r) parseEntity = nonNullCheck width >>=$ \ allNonNull -> if allNonNull then Just <$> parseEntity else skipColumns >>$ finish Nothing where width = fromIntegral (natVal @(Width a) Proxy) -- | Construct a 'QueryGenerator' using a 'B.Builder'. buildGen :: Oid -> (a -> B.Builder) -> QueryGenerator a buildGen typ builder = Gen typ (Just . BL.toStrict . B.toLazyByteString . builder) -- | Parse the contents of a column. parseContent :: Parser a -> RowParser 1 a parseContent p = processContent $ \ _ mbCnt -> do cnt <- mbCnt case endResult (parse p cnt) of Done _ r -> Just r _ -> Nothing where endResult (Partial f) = f B.empty endResult x = x -- | @boolean@ instance Entity Bool where type Width Bool = 1 genEntity = Gen (Oid 16) (\ v -> Just (if v then "t" else "f")) parseEntity = (`elem` ["t", "1", "true", "TRUE", "y", "yes", "YES", "on", "ON"]) <$> retrieveContent -- | Any integer instance Entity Integer where type Width Integer = 1 genEntity = buildGen (Oid 1700) B.integerDec parseEntity = parseContent (signed decimal) -- | Any integer instance Entity Int where type Width Int = 1 genEntity = buildGen (Oid 20) B.intDec parseEntity = parseContent (signed decimal) -- | Any integer instance Entity Int8 where type Width Int8 = 1 genEntity = buildGen (Oid 21) B.int8Dec parseEntity = parseContent (signed decimal) -- | Any integer instance Entity Int16 where type Width Int16 = 1 genEntity = buildGen (Oid 21) B.int16Dec parseEntity = parseContent (signed decimal) -- | Any integer instance Entity Int32 where type Width Int32 = 1 genEntity = buildGen (Oid 23) B.int32Dec parseEntity = parseContent (signed decimal) -- | Any integer instance Entity Int64 where type Width Int64 = 1 genEntity = buildGen (Oid 20) B.int64Dec parseEntity = parseContent (signed decimal) -- | Any unsigned integer instance Entity Natural where type Width Natural = 1 genEntity = With toInteger genEntity parseEntity = parseContent decimal -- | Any unsigned integer instance Entity Word where type Width Word = 1 genEntity = buildGen (Oid 1700) B.wordDec parseEntity = parseContent decimal -- | Any unsigned integer instance Entity Word8 where type Width Word8 = 1 genEntity = buildGen (Oid 21) B.word8Dec parseEntity = parseContent decimal -- | Any unsigned integer instance Entity Word16 where type Width Word16 = 1 genEntity = buildGen (Oid 23) B.word16Dec parseEntity = parseContent decimal -- | Any unsigned integer instance Entity Word32 where type Width Word32 = 1 genEntity = buildGen (Oid 20) B.word32Dec parseEntity = parseContent decimal -- | Any unsigned integer instance Entity Word64 where type Width Word64 = 1 genEntity = buildGen (Oid 1700) B.word64Dec parseEntity = parseContent decimal -- | Any floating-point number instance Entity Double where type Width Double = 1 genEntity = buildGen (Oid 1700) B.doubleDec parseEntity = parseContent double -- | Any floating-point number instance Entity Float where type Width Float = 1 genEntity = buildGen (Oid 1700) B.floatDec parseEntity = realToFrac @Double @Float <$> parseEntity -- | Any numeric type instance Entity Scientific where type Width Scientific = 1 genEntity = Gen (Oid 1700) (Just . buildByteString . formatScientific Fixed Nothing) parseEntity = parseContent scientific -- | @char@, @varchar@ or @text@ - UTF-8 encoded; does not allow NULL characters instance Entity String where type Width String = 1 genEntity = Gen (Oid 25) (Just . buildByteString . filter (/= '\NUL')) parseEntity = T.unpack <$> parseEntity -- | @char@, @varchar@ or @text@ - UTF-8 encoded; does not allow NULL characters instance Entity T.Text where type Width T.Text = 1 genEntity = Gen (Oid 25) (Just . T.encodeUtf8 . T.filter (/= '\NUL')) parseEntity = retrieveContent >>=$ \ input -> case T.decodeUtf8' input of Right x -> finish x _ -> cancel ColumnRejected -- | @char@, @varchar@ or @text@ - UTF-8 encoded; does not allow NULL characters instance Entity TL.Text where type Width TL.Text = 1 genEntity = With TL.toStrict genEntity parseEntity = TL.fromStrict <$> parseEntity -- | @bytea@ - byte array encoded in hex format instance Entity B.ByteString where type Width B.ByteString = 1 genEntity = buildGen (Oid 17) (\ value -> mconcat (B.string7 "\\x" : map showHex (B.unpack value))) where showHex n | n <= 0xF = B.char7 '0' <> B.word8Hex n | otherwise = B.word8Hex n parseEntity = parseContent (hexFormat <|> escapedFormat) where isHexChar x = (x >= 48 && x <= 57) -- 0 - 9 || (x >= 65 && x <= 70) -- A - F || (x >= 97 && x <= 102) -- a - f 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 -- 0 - 7 octCharToWord x | isOctChar x = x - 48 -- 0 | 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 type Width BL.ByteString = 1 genEntity = With BL.toStrict genEntity parseEntity = BL.fromStrict <$> parseEntity -- | @json@ or @jsonb@ instance Entity A.Value where type Width A.Value = 1 genEntity = Gen (Oid 114) (Just . BL.toStrict . A.encode) parseEntity = retrieveContent >>=$ \ input -> case A.decodeStrict input of Just x -> finish x _ -> cancel ColumnRejected