packages feed

dani-sqlite-0.1.0.0: lib/Sqlite/Query/FromField.hs

{-# LANGUAGE BlockArguments #-}

------------------------------------------------------------------------------

------------------------------------------------------------------------------

-- |
-- The 'FromField' typeclass, for converting a single value in a row
-- returned by a Sql query into a more useful Haskell representation.
--
-- A Haskell numeric type is considered to be compatible with all
-- Sqlite numeric types that are less accurate than it. For instance,
-- the Haskell 'Double' type is compatible with the Sqlite's 32-bit
-- @Int@ type because it can represent a @Int@ exactly. On the other hand,
-- since a 'Double' might lose precision if representing a 64-bit @BigInt@,
-- the two are /not/ considered compatible.
module Sqlite.Query.FromField
  ( FromField (..),
    FieldParser,
    ResultError (..),
    Field,
    fieldData,
    returnError,
  )
where

import Control.Exception (Exception, SomeException (..))
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as B
import Data.ByteString.Lazy qualified as LB
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Text qualified as T
import Data.Text.Lazy qualified as LT
import Data.Typeable (Typeable, typeOf)
import Data.Word (Word16, Word32, Word64, Word8)
import GHC.Float (double2Float)
import Sqlite as Base
import Sqlite.Query.Internal
import Sqlite.Query.Ok
import Sqlite.Query.Types

-- | 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,
        errHaskellType :: String,
        errMessage :: String
      }
  | -- | A Sql @NULL@ was encountered when the Haskell
    -- type did not permit it.
    UnexpectedNull
      { errSqlType :: 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,
        errHaskellType :: String,
        errMessage :: String
      }
  deriving (Eq, Show)

instance Exception ResultError

left :: (Exception a) => a -> Ok b
left = Errors . (: []) . SomeException

type FieldParser a = Field -> Ok a

-- | A type that may be converted from a Sql type.
class FromField a where
  fromField :: FieldParser a
  -- ^ Convert a Sql value to a Haskell value.
  --
  -- Returns a list of exceptions if the conversion fails.  In the case of
  -- library instances,  this will usually be a single 'ResultError',  but
  -- may be a 'UnicodeException'.
  --
  -- Implementations of 'fromField' should not retain any references to
  -- the 'Field' nor the 'ByteString' arguments after the result has
  -- been evaluated to WHNF.  Such a reference causes the entire
  -- @LibPQ.'PQ.Result'@ to be retained.
  --
  -- For example,  the instance for 'ByteString' uses 'B.copy' to avoid
  -- such a reference,  and that using bytestring functions such as 'B.drop'
  -- and 'B.takeWhile' alone will also trigger this memory leak.

instance (FromField a) => FromField (Maybe a) where
  fromField (Field SqlNull _) = pure Nothing
  fromField f = Just <$> fromField f

instance FromField Null where
  fromField (Field SqlNull _) = pure Null
  fromField f = returnError ConversionFailed f "data is not null"

takeInt :: (Num a, Typeable a) => Field -> Ok a
takeInt (Field (SqlInteger i) _) = Ok . fromIntegral $ i
takeInt f = returnError ConversionFailed f "need an int"

instance FromField Int8 where
  fromField = takeInt

instance FromField Int16 where
  fromField = takeInt

instance FromField Int32 where
  fromField = takeInt

instance FromField Int where
  fromField = takeInt

instance FromField Int64 where
  fromField = takeInt

instance FromField Integer where
  fromField = takeInt

instance FromField Word8 where
  fromField = takeInt

instance FromField Word16 where
  fromField = takeInt

instance FromField Word32 where
  fromField = takeInt

instance FromField Word64 where
  fromField = takeInt

instance FromField Word where
  fromField = takeInt

instance FromField Double where
  fromField (Field (SqlFloat flt) _) = Ok flt
  fromField f = returnError ConversionFailed f "expecting an SqlFloat column type"

instance FromField Float where
  fromField (Field (SqlFloat flt) _) = Ok . double2Float $ flt
  fromField f = returnError ConversionFailed f "expecting an SqlFloat column type"

instance FromField Bool where
  fromField f@(Field (SqlInteger b) _)
    | (b == 0) || (b == 1) = Ok (b /= 0)
    | otherwise = returnError ConversionFailed f ("bool must be 0 or 1, got " ++ show b)
  fromField f = returnError ConversionFailed f "expecting an SqlInteger column type"

instance FromField T.Text where
  fromField f@(Field sqlData _) = case sqlData of
    SqlText v -> Ok v
    SqlInteger v -> Ok $ T.pack $ show v
    SqlFloat v -> Ok $ T.pack $ show v
    _ -> returnError ConversionFailed f "need convertible to text"

instance FromField LT.Text where
  fromField f = LT.fromStrict <$> fromField f

instance FromField [Char] where
  fromField f = T.unpack <$> fromField f

instance FromField ByteString where
  fromField (Field (SqlBlob blb) _) = Ok blb
  fromField f = returnError ConversionFailed f "expecting SqlBlob column type"

instance FromField LB.ByteString where
  fromField (Field (SqlBlob blb) _) = Ok . LB.fromChunks $ [blb]
  fromField f = returnError ConversionFailed f "expecting SqlBlob column type"

instance FromField SqlData where
  fromField :: FieldParser SqlData
  fromField f = Ok (fieldData f)

fieldTypename :: Field -> String
fieldTypename = B.unpack . gettypename . result

-- | Return the actual Sql data for a database field.  This allows
-- user-defined 'FromField' instances to access the Sql data
-- associated with a field being parsed.
fieldData :: Field -> SqlData
fieldData = result

-- | 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 -> String -> String -> err) ->
  Field ->
  String ->
  Ok a
returnError mkErr f =
  left
    . mkErr
      (fieldTypename f)
      (show (typeOf (undefined :: a)))