packages feed

hpgsql-0.1.0.0: src/Hpgsql/Types.hs

module Hpgsql.Types
  ( Only (..),
    Aeson (..),
    PgJson, -- Do not export ctor
    PGArray (..),
    (:.) (..),
    pgJsonByteString,
  )
where

import Control.Monad (replicateM)
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Encoding as AesonInternal
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Lazy as LBS
import Data.Tuple.Only (Only (..))
import Data.Typeable (Proxy (..), Typeable)
import Hpgsql.Builder (BinaryField (..))
import Hpgsql.Encoding (FieldDecoder (..), FieldEncoder (..), FieldInfo (..), FromPgField (..), FromPgRow (..), RowEncoder (..), ToPgField (..), ToPgRow (..), arrayField, toPgVectorField)
import Hpgsql.TypeInfo (EncodingContext (..), TypeInfo (..), jsonOid, jsonbOid, lookupTypeByOid)

-- | Encodes a Haskell list as a postgres array. You can also use `Vector` if you prefer.
-- The reason for this type instead of allowing @[a]@ to be a field is that an instance
-- for @[a]@ would require an overlappable instance for @String@, and that is not ideal.
newtype PGArray a = PGArray {fromPGArray :: [a]}
  deriving (Eq, Ord, Read, Show, Functor)

instance forall a. (ToPgField a) => ToPgField (PGArray a) where
  fieldEncoder =
    let fe = fieldEncoder @a
     in FieldEncoder
          { toTypeOid = \encodingContext -> do
              elOid <- fe.toTypeOid encodingContext
              arrayTypInfo <- lookupTypeByOid elOid encodingContext.typeInfoCache
              arrayTypInfo.oidOfArrayType,
            toPgField = \encCtx -> toPgVectorField encCtx . fromPGArray
          }

instance forall a. (FromPgField a) => FromPgField (PGArray a) where
  fieldDecoder = PGArray <$> arrayField replicateM fieldDecoder

-- | A way to compose two rows.
data h :. t = !h :. !t deriving (Eq, Ord, Show, Read)

infixr 3 :.

instance forall a b. (ToPgRow a, ToPgRow b) => ToPgRow (a :. b) where
  rowEncoder =
    let !re1 = rowEncoder @a
        !re2 = rowEncoder @b
     in RowEncoder
          { toPgParams = \(a :. b) -> re1.toPgParams a ++ re2.toPgParams b,
            toTypeOids = \_ -> re1.toTypeOids (Proxy @a) ++ re2.toTypeOids (Proxy @b),
            toBinaryCopyBytes = \encCtx ->
              let !toBytes1 = re1.toBinaryCopyBytes encCtx
                  !toBytes2 = re2.toBinaryCopyBytes encCtx
               in \(a :. b) -> toBytes1 a <> toBytes2 b
          }

instance (FromPgRow a, FromPgRow b) => FromPgRow (a :. b) where
  rowDecoder = (:.) <$> rowDecoder <*> rowDecoder

-- | A JSON type that does not incur the costs of deserializing
-- in its `FromPgField` instance because it assumes postgres only generates
-- valid JSON. Useful for extra performance if its opaqueness is not a problem.
-- Although it does have a `toJSON` method, using it will incur a
-- deserialization cost, so if you find yourself using that too much consider just using
-- `Aeson.Value` or the `Aeson` newtype instead of this.
newtype PgJson = PgJson ByteString

instance ToJSON PgJson where
  toJSON (PgJson bs) = case Aeson.eitherDecodeStrict' bs of
    Left err -> error $ "Bug in Hpgsql. PgJson not valid JSON: " ++ err
    Right v -> v

  toEncoding (PgJson bs) = AesonInternal.unsafeToEncoding (Builder.byteString bs)

-- | A valid UTF8 representation of the JSON value.
pgJsonByteString :: PgJson -> ByteString
pgJsonByteString (PgJson bs) = bs

instance FromPgField PgJson where
  fieldDecoder =
    FieldDecoder
      { fieldValueDecoder =
          \FieldInfo {fieldTypeOid} ->
            let
              -- jsonb has a byte prepended to the contents and json does not
              !fixJsonb = if fieldTypeOid == jsonbOid then BS.drop 1 else Prelude.id
             in
              \case
                Just bs -> Right $ PgJson $ fixJsonb bs
                Nothing -> Left "Cannot decode SQL null as the Haskell PgJson type. Use a `Maybe PgJson` if you want SQL nulls",
        allowedPgTypes = (`elem` [jsonOid, jsonbOid]) . fieldTypeOid
      }

-- | A newtype wrapper to decode a JSON value with Aeson
-- into your type (from either json or jsonb), and to encode
-- to jsonb.
newtype Aeson a = Aeson {getAeson :: a}
  deriving (Eq, Show, Read, Typeable, Functor)

instance (FromJSON a) => FromPgField (Aeson a) where
  fieldDecoder =
    FieldDecoder
      { fieldValueDecoder =
          \FieldInfo {fieldTypeOid} ->
            let
              -- jsonb has a byte prepended to the contents and json does not
              !fixJsonb = if fieldTypeOid == jsonbOid then BS.drop 1 else Prelude.id
             in
              \case
                Just bs -> case Aeson.decodeStrict $ fixJsonb bs of
                  Just v -> Right $ Aeson v
                  Nothing -> Left "Failed to decode postgres JSON value into your `Aeson a` type. Are you sure it's proper JSON?"
                Nothing -> Left "Cannot decode SQL null as a Haskell (Aeson a) type. Use a `Maybe (Aeson a)` if you want SQL nulls",
        allowedPgTypes = (`elem` [jsonOid, jsonbOid]) . fieldTypeOid
      }

instance (ToJSON a) => ToPgField (Aeson a) where
  fieldEncoder =
    FieldEncoder
      { toTypeOid = \_ -> Just jsonbOid,
        toPgField = \_ (Aeson v) ->
          NotNull $ BS.cons 1 (LBS.toStrict $ Aeson.encode v)
      }