hpgsql-simple-compat-0.1.0.0: src/Database/PostgreSQL/Simple/FromField.hs
-- |
-- Module: Database.PostgreSQL.Simple.FromField
-- Copyright: (c) 2011 MailRank, Inc.
-- (c) 2011-2013 Leon P Smith
-- License: BSD3
-- Maintainer: Leon P Smith <leon@melding-monads.com>
-- Stability: experimental
--
-- The 'FromField' typeclass, for converting a single value in a row
-- returned by a SQL query into a more useful Haskell representation.
-- Note that each instance of 'FromField' is documented by a list of
-- compatible postgresql types.
--
-- A Haskell numeric type is considered to be compatible with all
-- PostgreSQL numeric types that are less accurate than it. For instance,
-- the Haskell 'Double' type is compatible with the PostgreSQL's 32-bit
-- @int@ type because it can represent a @int@ exactly. On the other hand,
-- since a 'Double' might lose precision if representing PostgreSQL's 64-bit
-- @bigint@, the two are /not/ considered compatible.
--
-- Note that the 'Float' and 'Double' instances use attoparsec's 'double'
-- conversion routine, which sacrifices some accuracy for speed. If you
-- need accuracy, consider first converting data to a 'Scientific' or 'Rational'
-- type, and then converting to a floating-point type. If you are defining
-- your own 'Database.PostgreSQL.Simple.FromRow.FromRow' instances, this can be
-- achieved simply by
-- @'fromRational' '<$>' 'Database.PostgreSQL.Simple.FromRow.field'@, although
-- this idiom is additionally compatible with PostgreSQL's @int8@ and @numeric@
-- types. If this is unacceptable, you may find
-- 'Database.PostgreSQL.Simple.FromRow.fieldWith' useful.
--
-- Also note that while converting to a 'Double' through the 'Scientific' type
-- is likely somewhat faster than converting through the 'Rational' type,
-- the 'Scientific' type has no way to represent @NaN@ and @±Infinity@ values.
-- Thus, if you need precision conversion of regular floating point values
-- and the possibility of receiving these special values from the backend,
-- stick with 'Rational'.
--
-- Because 'FromField' is a typeclass, one may provide conversions to
-- additional Haskell types without modifying hpgsql-simple-compat. This is
-- particularly useful for supporting PostgreSQL types that hpgsql-simple-compat
-- does not support out-of-box. Here's an example of what such an instance
-- might look like for a UUID type that implements the @Read@ class:
--
-- @
-- import Data.UUID ( UUID )
-- import Database.PostgreSQL.Simple.FromField
-- ( FromField (fromField) , typeOid, returnError, ResultError (..) )
-- import Database.PostgreSQL.Simple.TypeInfo.Static (typoid, uuid)
-- import qualified Data.ByteString.Char8 as B
--
-- instance FromField UUID where
-- fromField f mdata =
-- if typeOid f /= typoid uuid
-- then returnError Incompatible f \"\"
-- else case B.unpack \`fmap\` mdata of
-- Nothing -> returnError UnexpectedNull f \"\"
-- Just dat ->
-- case [ x | (x,t) <- reads dat, (\"\",\"\") <- lex t ] of
-- [x] -> return x
-- _ -> returnError ConversionFailed f dat
-- @
--
-- Note that because PostgreSQL's @uuid@ type is built into postgres and is
-- not provided by an extension, the 'typeOid' of @uuid@ does not change and
-- thus we can examine it directly. One could hard-code the type oid, or
-- obtain it by other means, but in this case we simply pull it out of the
-- static table provided by hpgsql-simple-compat.
--
-- On the other hand if the type is provided by an extension, such as
-- @PostGIS@ or @hstore@, then the 'typeOid' is not stable and can vary from
-- database to database. In this case it is recommended that FromField
-- instances use 'typename' instead.
module Database.PostgreSQL.Simple.FromField
( FromField (..),
FromPgField (..),
FieldParser,
Conversion (..),
-- conversionMap,
conversionError,
ResultError (..),
returnError,
Field,
typename,
TypeInfo (..),
Attribute (..),
typeInfo,
typeInfoByOid,
name,
typeOid,
PQ.Oid (..),
PQ.Format (..),
optionalField,
fromFieldJSONByteString,
fromJSONField,
)
where
import Control.Exception (Exception (fromException, toException))
import qualified Data.Aeson as Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as LBS
import Data.CaseInsensitive (CI)
import Data.Int (Int16, Int32, Int64)
import Data.Scientific (Scientific)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text.Lazy as LT
import Data.Time.Calendar.Compat (Day)
import Data.Time.Compat (UTCTime)
import Data.Time.LocalTime.Compat (CalendarDiffTime, LocalTime, TimeOfDay, ZonedTime)
import Data.Typeable (Typeable, typeOf)
import Data.UUID.Types (UUID)
import Data.Vector (Vector)
import qualified Database.PostgreSQL.LibPQ as PQ
import Database.PostgreSQL.Simple.HpgsqlUtils
import Database.PostgreSQL.Simple.Internal (postgresqlExceptionFromException, postgresqlExceptionToException)
import Database.PostgreSQL.Simple.Ok (Ok (..))
import Database.PostgreSQL.Simple.TypeInfo as TI
import Database.PostgreSQL.Simple.TypeInfo.Types (fromHpgsqlTypeInfo)
import Database.PostgreSQL.Simple.Types (Binary (..))
import Hpgsql.Encoding (EncodingContext (..), FieldInfo (..), FromPgField (..))
import qualified Hpgsql.Encoding as Hpgsql
import Hpgsql.Time (Unbounded (..))
import Hpgsql.TypeInfo (Oid, lookupTypeByOid)
import Hpgsql.Types (Aeson, PGArray (..), getAeson, pgJsonByteString)
-- | Exception thrown if conversion from a SQL value to a Haskell
-- value fails.
data ResultError
= -- | The SQL and Haskell types are not compatible.
Incompatible
{ errSQLType :: String,
errSQLTableOid :: Maybe PQ.Oid,
errSQLField :: String,
errHaskellType :: String,
errMessage :: String
}
| -- | A SQL @NULL@ was encountered when the Haskell
-- type did not permit it.
UnexpectedNull
{ errSQLType :: String,
errSQLTableOid :: Maybe PQ.Oid,
errSQLField :: String,
errHaskellType :: String,
errMessage :: String
}
| -- | The SQL value could not be parsed, or could not
-- be represented as a valid Haskell value, or an
-- unexpected low-level error occurred (e.g. mismatch
-- between metadata and actual data in a row).
ConversionFailed
{ errSQLType :: String,
errSQLTableOid :: Maybe PQ.Oid,
errSQLField :: String,
errHaskellType :: String,
errMessage :: String
}
deriving (Eq, Show, Typeable)
instance Exception ResultError where
toException = postgresqlExceptionToException
fromException = postgresqlExceptionFromException
-- | This might be very different from postgresql-simple.
conversionError :: (Exception err) => err -> Conversion a
conversionError ex = Conversion $ \_ -> Errors [toException ex]
left :: (Exception a) => a -> Conversion b
left = conversionError
class FromField a where
fromField :: FieldParser a
default fromField :: (Hpgsql.FromPgField a) => FieldParser a
fromField =
let dec = Hpgsql.fieldDecoder
in \f ->
if (Hpgsql.allowedPgTypes dec) f
then \mbs -> Conversion $ \_encCtx -> case (Hpgsql.fieldValueDecoder dec) f mbs of
Right v -> Ok v
Left err -> Errors [toException $ userError err]
else \_ -> Conversion $ \_encCtx -> Errors [toException $ userError "Invalid type OID for FromField instance"]
instance FromField ()
instance FromField Int
instance FromField Int16
instance FromField Int32
instance FromField Int64
instance FromField Integer
instance FromField Oid
instance FromField Scientific
instance FromField Rational
instance FromField Float
instance FromField Double
instance FromField Bool
instance FromField Char
instance FromField ByteString
instance FromField LBS.ByteString
instance FromField Text
instance FromField LT.Text
instance FromField String
instance FromField UTCTime
instance FromField ZonedTime
instance FromField LocalTime
instance FromField TimeOfDay
instance FromField Day
instance FromField CalendarDiffTime
instance FromField Aeson.Value
instance FromField UUID
instance FromField (CI Text)
instance FromField (CI LT.Text)
instance FromField (CI String)
instance FromField (Unbounded Day)
instance FromField (Unbounded UTCTime)
instance FromField (Unbounded ZonedTime)
instance (FromField a) => FromField (Maybe a) where
fromField = optionalField fromField
-- instance (FromField a) => FromField (Vector a)
-- instance (FromField a) => FromField (PGArray a)
instance (Hpgsql.FromPgField a) => FromField (PGArray a)
instance (Hpgsql.FromPgField a) => FromField (Vector a)
instance {-# OVERLAPPING #-} (Hpgsql.FromPgField a) => FromField (Vector (Vector a))
instance FromField (Binary ByteString)
instance (Aeson.FromJSON a) => FromField (Aeson a)
typeOid :: Field -> Oid
typeOid field = field.fieldTypeOid
typename :: Field -> Conversion ByteString
typename field = typname <$> typeInfo field
typeInfo :: Field -> Conversion TypeInfo
typeInfo colInfo = typeInfoByOid colInfo.fieldTypeOid
typeInfoByOid :: PQ.Oid -> Conversion TypeInfo
typeInfoByOid oid = Conversion $ \encCtx -> do
case lookupTypeByOid oid encCtx.typeInfoCache of
Nothing -> Errors [toException $ userError $ "Type OID " ++ show oid ++ " not found in TypeInfoCache"]
Just t -> Ok (fromHpgsqlTypeInfo encCtx t)
-- | Returns the name of the column. This is often determined by a table
-- definition, but it can be set using an @as@ clause.
name :: Field -> Maybe ByteString
name f = encodeUtf8 <$> fieldName f
-- | Given one of the constructors from 'ResultError', the field,
-- and an 'errMessage', this fills in the other fields in the
-- exception value and returns it in a 'Left . SomeException'
-- constructor.
returnError ::
forall a err.
(Typeable a, Exception err) =>
(String -> Maybe PQ.Oid -> String -> String -> String -> err) ->
Field ->
String ->
Conversion a
returnError mkErr f msg = do
typnam <- typename f
left $
mkErr
(B8.unpack typnam)
(error "no tableOid in hpgsql")
-- (maybe "" B8.unpack (name f))
"column name not available in hpgsql-simple-compat"
(show (typeOf (undefined :: a)))
msg
-- | For dealing with SQL @null@ values outside of the 'FromField' class.
-- Alternatively, one could use 'Control.Applicative.optional', but that
-- also turns type and conversion errors into 'Nothing', whereas this is
-- more specific and turns only @null@ values into 'Nothing'.
optionalField :: FieldParser a -> FieldParser (Maybe a)
optionalField p f mv =
case mv of
Nothing -> pure Nothing
Just _ -> Just <$> p f mv
{-# INLINE optionalField #-}
fromJSONField :: (Aeson.FromJSON a) => FieldParser a
fromJSONField f mbs = getAeson <$> fromField f mbs
fromFieldJSONByteString :: FieldParser ByteString
fromFieldJSONByteString = fromHpgsqlFieldDecoder $ pgJsonByteString <$> fieldDecoder