packages feed

sq-0.1: lib/Sq/Decoders.hs

module Sq.Decoders
   ( Decode (..)
   , ErrDecode (..)
   , decodeRefine
   , DecodeDefault (..)
   , decodeMaybe
   , decodeEither
   , decodeNS
   , decodeBoundedIntegral
   , decodeBinary
   , decodeBinary'
   , decodeRead
   , decodeAeson
   , decodeAeson'
   ) where

import Control.Applicative
import Control.Exception.Safe qualified as Ex
import Control.Monad
import Control.Monad.Catch qualified as Ex (MonadThrow (..))
import Control.Monad.Trans.Reader
import Data.Aeson qualified as Ae
import Data.Aeson.Key qualified as Aek
import Data.Aeson.Parser qualified as Aep
import Data.Aeson.Types qualified as Ae
import Data.Attoparsec.ByteString qualified as AB
import Data.Attoparsec.Text qualified as AT
import Data.Attoparsec.Time qualified as AT8601
import Data.Bifunctor
import Data.Binary qualified as Bin
import Data.Binary.Get qualified as Bin
import Data.Bits
import Data.ByteString qualified as B
import Data.ByteString.Builder.Prim.Internal (caseWordSize_32_64)
import Data.ByteString.Lazy qualified as BL
import Data.Fixed
import Data.Int
import Data.Proxy
import Data.SOP qualified as SOP
import Data.Scientific qualified as Sci
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Unsafe qualified as T
import Data.Time qualified as Time
import Data.Time.Clock.POSIX qualified as Time
import Network.URI qualified as URI

import Data.Time.Format.ISO8601 qualified as Time
import Data.UUID.Types qualified as UUID
import Data.Word
import Database.SQLite3 qualified as S
import GHC.Float (double2Float, float2Double)
import GHC.Stack
import Numeric.Natural
import Text.Read (readEither)

import Sq.Null (Null)
import Sq.Support

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

-- | How to decode a single SQLite column value into a Haskell value of type
-- @a@.
newtype Decode a
   = -- | Decode a 'S.SQLData' value into a value of type @a@.
     Decode (S.SQLData -> Either ErrDecode a)
   deriving
      (Functor, Applicative, Monad)
      via ReaderT S.SQLData (Either ErrDecode)

-- | @'mempty' = 'pure' 'mempty'@
instance (Monoid a) => Monoid (Decode a) where
   mempty = pure mempty
   {-# INLINE mempty #-}

-- | @('<>') == 'liftA2' ('<>')@
instance (Semigroup a) => Semigroup (Decode a) where
   (<>) = liftA2 (<>)
   {-# INLINE (<>) #-}

instance Ex.MonadThrow Decode where
   throwM = Decode . const . Left . ErrDecode_Fail . Ex.toException

instance MonadFail Decode where
   fail = Ex.throwString
   {-# INLINE fail #-}

-- | Leftmost result on success, rightmost error on failure.
instance Alternative Decode where
   empty = fail "empty"
   {-# INLINE empty #-}
   (<|>) = mplus
   {-# INLINE (<|>) #-}

-- | Leftmost result on success, rightmost error on failure.
instance MonadPlus Decode where
   mzero = fail "mzero"
   {-# INLINE mzero #-}
   mplus (Decode l) (Decode r) = Decode \s ->
      either (\_ -> r s) pure (l s)
   {-# INLINE mplus #-}

-- | See v'Decode'.
data ErrDecode
   = -- | Received 'S.ColumnType', expected 'S.ColumnType's.
     ErrDecode_Type S.ColumnType [S.ColumnType]
   | ErrDecode_Fail Ex.SomeException
   deriving stock (Show)
   deriving anyclass (Ex.Exception)

errDecodeFailString :: (HasCallStack) => String -> ErrDecode
errDecodeFailString s =
   ErrDecode_Fail $ Ex.toException $ Ex.StringException s ?callStack
{-# INLINE errDecodeFailString #-}

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

sqlDataColumnType :: S.SQLData -> S.ColumnType
sqlDataColumnType = \case
   S.SQLInteger _ -> S.IntegerColumn
   S.SQLFloat _ -> S.FloatColumn
   S.SQLText _ -> S.TextColumn
   S.SQLBlob _ -> S.BlobColumn
   S.SQLNull -> S.NullColumn

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

-- | A convenience function for refining a 'Decode'r through a function that
-- may fail with a 'String' error message. The 'CallStack' is preserved.
--
-- If you need a more sophisticated refinement, use the 'Decode' constructor.
decodeRefine
   :: (HasCallStack)
   => (a -> Either String b)
   -> Decode a
   -> Decode b
decodeRefine f (Decode g) = Decode \raw -> do
   a <- g raw
   first errDecodeFailString (f a)

--------------------------------------------------------------------------------
-- Core decodes

-- | Default way to decode a SQLite value into a Haskell value of type @a@.
--
-- If there there exist also a 'Sq.EncodeDefault' instance for @a@, then it
-- must roundtrip with the 'Sq.DecodeDefault' instance for @a@.
class DecodeDefault a where
   -- | Default way to decode a SQLite value into a Haskell value of type @a@.
   decodeDefault :: Decode a

-- | Literal 'S.SQLData' 'Decode'.
instance DecodeDefault S.SQLData where
   decodeDefault = Decode Right
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Int64 where
   {-# INLINE decodeDefault #-}
   decodeDefault = Decode \case
      S.SQLInteger x -> Right x
      x -> Left $ ErrDecode_Type (sqlDataColumnType x) [S.IntegerColumn]

-- | 'S.FloatColumn'.
instance DecodeDefault Double where
   {-# INLINE decodeDefault #-}
   decodeDefault = Decode \case
      S.SQLFloat x -> Right x
      x -> Left $ ErrDecode_Type (sqlDataColumnType x) [S.FloatColumn]

-- | 'S.TextColumn'.
instance DecodeDefault T.Text where
   {-# INLINE decodeDefault #-}
   decodeDefault = Decode \case
      S.SQLText x -> Right x
      x -> Left $ ErrDecode_Type (sqlDataColumnType x) [S.TextColumn]

-- | 'S.BlobColumn'.
instance DecodeDefault B.ByteString where
   {-# INLINE decodeDefault #-}
   decodeDefault = Decode \case
      S.SQLBlob x -> Right x
      x -> Left $ ErrDecode_Type (sqlDataColumnType x) [S.BlobColumn]

-- | 'S.NullColumn'.
instance DecodeDefault Null where
   {-# INLINE decodeDefault #-}
   decodeDefault = Decode \case
      S.SQLNull -> Right mempty
      x -> Left $ ErrDecode_Type (sqlDataColumnType x) [S.NullColumn]

--------------------------------------------------------------------------------
-- Extra decodes

-- | 'S.TextColumn'.
instance DecodeDefault TL.Text where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/TextLazy" #-}
      (TL.fromStrict <$> decodeDefault)

-- | 'S.TextColumn'.
instance DecodeDefault Char where
   decodeDefault =
      {-# SCC "decodeDefault/Char" #-}
      ( flip decodeRefine decodeDefault \t ->
         if T.length t == 1
            then Right (T.unsafeHead t)
            else Left "Expected single character string"
      )

-- | 'S.TextColumn'.
instance DecodeDefault String where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/String" #-}
      (T.unpack <$> decodeDefault)

-- | 'S.BlobColumn'.
instance DecodeDefault BL.ByteString where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/ByteStringLazy" #-}
      (BL.fromStrict <$> decodeDefault)

-- | See 'decodeMaybe'.
instance (DecodeDefault a) => DecodeDefault (Maybe a) where
   decodeDefault = decodeMaybe decodeDefault
   {-# INLINE decodeDefault #-}

-- | Attempt to decode @a@ first, or a 'S.NullColumn' as 'Nothing' otherwise.
decodeMaybe :: Decode a -> Decode (Maybe a)
decodeMaybe (Decode fa) = Decode \d -> case fa d of
   Right a -> Right (Just a)
   Left e -> case d of
      S.SQLNull -> Right Nothing
      _ -> Left e

-- | See 'decodeEither'.
instance
   (DecodeDefault a, DecodeDefault b)
   => DecodeDefault (Either a b)
   where
   decodeDefault = decodeEither decodeDefault decodeDefault
   {-# INLINE decodeDefault #-}

-- | @
-- 'decodeEither' da db = fmap 'Left' da '<|>' fmap 'Right' db
-- @
--
-- __WARNING__ This is probably not what you are looking for. The
-- underlying 'S.SQLData' doesn't carry any /tag/ for discriminating
-- between @a@ and @b@.
decodeEither :: Decode a -> Decode b -> Decode (Either a b)
decodeEither da db = fmap Left da <|> fmap Right db

-- | See 'decodeNS'.
instance
   (SOP.All DecodeDefault xs)
   => DecodeDefault (SOP.NS SOP.I xs)
   where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/SOP.NS" #-}
      decodeNS (SOP.hcpure (Proxy @DecodeDefault) decodeDefault)

-- | Like 'decodeEither', but for arbitraryly large "Data.SOP".'NS' sums.
--
-- __WARNING__ This is probably not what you are looking for. The underlying
-- 'S.SQLData' doesn't carry any /tag/ for discriminating among @xs@.
decodeNS :: SOP.NP Decode xs -> Decode (SOP.NS SOP.I xs)
decodeNS = hasum

-- | 'S.IntegerColumn', 'S.FloatColumn', 'S.TextColumn'
-- depicting a literal integer.
instance DecodeDefault Integer where
   decodeDefault =
      {-# SCC "decodeDefault/Integer" #-}
      ( Decode \case
         S.SQLInteger i -> Right (fromIntegral i)
         S.SQLText t -> case scientificFromText t of
            Just s -> case Sci.floatingOrInteger s of
               Right ~i
                  | sciIntegerMin <= s && s <= sciIntegerMax -> Right i
                  | otherwise -> Left $ errDecodeFailString "Integer too large"
               Left (_ :: Double) -> Left $ errDecodeFailString "Not an integer"
            Nothing -> Left $ errDecodeFailString "Malformed number"
         S.SQLFloat d
            | isNaN d -> Left $ errDecodeFailString "NaN"
            | isInfinite d -> Left $ errDecodeFailString "Infinity"
            | (i, 0) <- properFraction d, d == fromIntegral i -> Right i
            | otherwise -> Left $ errDecodeFailString "Lossy conversion"
         x -> Left $ ErrDecode_Type (sqlDataColumnType x) do
            [S.IntegerColumn, S.FloatColumn, S.TextColumn]
      )

-- | Some probably large enough numbers.
sciIntegerMax :: Sci.Scientific
sciIntegerMax = Sci.scientific 1 (fromIntegral (maxBound :: Int16))

-- | Some probably large enough numbers.
sciIntegerMin :: Sci.Scientific
sciIntegerMin = Sci.scientific (-1) (fromIntegral (maxBound :: Int16))

-- | 'S.IntegerColumn', 'S.FloatColumn', 'S.TextColumn'.
decodeBoundedIntegral :: forall a. (Integral a, Bounded a, Bits a) => Decode a
decodeBoundedIntegral = Decode \case
   S.SQLInteger i -> f i
   S.SQLText t -> case scientificFromText t of
      Just s
         | Sci.isInteger s -> case Sci.toBoundedInteger s of
            Just a -> Right a
            Nothing -> Left $ errDecodeFailString "Overflow or underflow"
         | otherwise -> Left $ errDecodeFailString "Not an integer"
      Nothing -> Left $ errDecodeFailString "Malformed number"
   S.SQLFloat d
      | isNaN d -> Left $ errDecodeFailString "NaN"
      | isInfinite d -> Left $ errDecodeFailString "Infinity"
      | (i :: Integer, 0) <- properFraction d ->
         case f i of
            Right a
               | d == fromIntegral a -> Right a
               | otherwise -> Left $ errDecodeFailString "Lossy conversion"
            Left e -> Left e
      | otherwise -> Left $ errDecodeFailString "Not an integer"
   x -> Left $ ErrDecode_Type (sqlDataColumnType x) do
      [S.IntegerColumn, S.FloatColumn, S.TextColumn]
  where
   f :: (Bits i, Integral i) => i -> Either ErrDecode a
   f i = case toIntegralSized i of
      Just a -> Right a
      Nothing -> Left $ errDecodeFailString "Overflow or underflow"

-- | 'S.IntegerColumn'.
instance DecodeDefault Int8 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Word8 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Int16 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Word16 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Int32 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Word32 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn' if it fits in 'Int64', otherwise 'S.TextColumn'.
instance DecodeDefault Word where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn' if it fits in 'Int64', otherwise 'S.TextColumn'.
instance DecodeDefault Word64 where
   decodeDefault = decodeBoundedIntegral
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn'.
instance DecodeDefault Int where
   decodeDefault =
      caseWordSize_32_64
         decodeBoundedIntegral
         (fromIntegral <$> decodeDefault @Int64)
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn' if it fits in 'Int64', otherwise 'S.TextColumn'.
instance DecodeDefault Natural where
   decodeDefault =
      decodeRefine
         (note "Underflow" . toIntegralSized)
         (decodeDefault @Integer)
   {-# INLINE decodeDefault #-}

-- | 'S.IntegerColumn' and 'S.FloatColumn' only.
--
-- @0@ is 'False', every other number is 'True'.
instance DecodeDefault Bool where
   decodeDefault =
      {-# SCC "decodeDefault/Bool" #-}
      ( Decode \case
         S.SQLInteger x -> Right (x /= 0)
         S.SQLFloat x -> Right (x /= 0)
         x ->
            Left $
               ErrDecode_Type
                  (sqlDataColumnType x)
                  [S.IntegerColumn, S.FloatColumn]
      )

-- | Like for 'Time.ZonedTime'.
instance DecodeDefault Time.UTCTime where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/UTCTime" #-}
      (Time.zonedTimeToUTC <$> decodeDefault)

-- | 'S.TextColumn' ('Time.ISO8601', or seconds since Epoch with optional decimal
-- part of up to picosecond precission), or 'S.Integer' (seconds since Epoch).
--
-- TODO: Currently precission over picoseconds is successfully parsed but
-- silently floored. This is an issue in "Data.Time.Format.ISO8601". Fix.
instance DecodeDefault Time.ZonedTime where
   decodeDefault =
      {-# SCC "decodeDefault/ZonedTime" #-}
      ( Decode \case
         S.SQLText t -> case AT.parseOnly pzt t of
            Right zt -> Right zt
            Left e -> first ErrDecode_Fail do
               Ex.throwString $ "Invalid timestamp: " <> show e
         S.SQLInteger i ->
            Right $
               Time.utcToZonedTime Time.utc $
                  Time.posixSecondsToUTCTime $
                     fromIntegral i
         x ->
            Left $
               ErrDecode_Type
                  (sqlDataColumnType x)
                  [S.IntegerColumn, S.TextColumn]
      )
     where
      pzt =
         mplus
            ({-# SCC zonedTime #-} AT8601.zonedTime <* AT.endOfInput)
            ({-# SCC psecs #-} psecs <* AT.endOfInput)
      psecs = do
         s <- AT.scientific
         let pico :: Pico = MkFixed $ floor (s * 1_000_000_000_000)
             ndt :: Time.NominalDiffTime = Time.secondsToNominalDiffTime pico
         pure $ Time.utcToZonedTime Time.utc $ Time.posixSecondsToUTCTime ndt

-- | 'Time.ISO8601' in a @'S.TextColumn'.
instance DecodeDefault Time.LocalTime where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/LocalTime" #-}
      ( decodeRefine
         (AT.parseOnly (AT8601.localTime <* AT.endOfInput))
         decodeDefault
      )

-- | 'Time.ISO8601' in a @'S.TextColumn'.
instance DecodeDefault Time.Day where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/Day" #-}
      ( decodeRefine
         (AT.parseOnly (AT8601.day <* AT.endOfInput))
         decodeDefault
      )

-- | 'Time.ISO8601' in a @'S.TextColumn'.
--
-- TODO: Currently precission over picoseconds is successfully parsed but
-- silently floored. This is an issue in "Data.Time.Format.ISO8601". Fix.
instance DecodeDefault Time.TimeOfDay where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/TimeOfDay" #-}
      ( decodeRefine
         (AT.parseOnly (AT8601.timeOfDay <* AT.endOfInput))
         decodeDefault
      )

-- | 'Time.ISO8601' in a @'S.TextColumn'.
--
-- TODO: Currently precission over picoseconds is successfully parsed but
-- silently floored. This is an issue in "Data.Time.Format.ISO8601". Fix.
instance DecodeDefault Time.CalendarDiffDays where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/CalendarDiffDays" #-}
      (decodeDefault >>= Time.iso8601ParseM)

-- | 'Time.ISO8601' in a @'S.TextColumn'.
--
-- TODO: Currently precission over picoseconds is successfully parsed but
-- silently floored. This is an issue in "Data.Time.Format.ISO8601". Fix.
instance DecodeDefault Time.CalendarDiffTime where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/CalendarDiffTime" #-}
      (decodeDefault >>= Time.iso8601ParseM)

-- | 'Time.ISO8601' in a @'S.TextColumn'.
instance DecodeDefault Time.TimeZone where
   {-# INLINE decodeDefault #-}
   decodeDefault =
      {-# SCC "decodeDefault/TimeZone" #-}
      ( flip decodeRefine decodeDefault \t ->
         case AT.parseOnly (AT8601.timeZone <* AT.endOfInput) t of
            Right (Just tz) -> Right tz
            Right Nothing -> Left "Not a valid time zone"
            Left s -> Left ("Not a valid time zone: " <> show s)
      )

-- | 'S.FloatColumn'.
instance DecodeDefault Float where
   decodeDefault =
      {-# SCC "decodeDefault/Float" #-}
      ( flip decodeRefine decodeDefault \d -> do
         let f = double2Float d
         if float2Double f == d
            then Right f
            else Left "Lossy conversion"
      )

-- | 'S.TextColumn'.
instance DecodeDefault UUID.UUID where
   decodeDefault =
      {-# SCC "decodeDefault/UUID" #-}
      ( decodeRefine
         (note "Not valid UUID" . UUID.fromText)
         decodeDefault
      )

-- | 'S.TextColumn'.
instance DecodeDefault Ae.Value where
   decodeDefault =
      {-# SCC "decodeDefault/Ae.Value" #-}
      (decodeRefine Ae.eitherDecodeStrictText decodeDefault)

-- | 'S.TextColumn'.
instance DecodeDefault Aek.Key where
   decodeDefault = Aek.fromText <$> decodeDefault

-- | 'S.IntegerColumn', 'S.FloatColumn', 'S.TextColumn'.
instance forall e. (HasResolution e) => DecodeDefault (Fixed e) where
   decodeDefault =
      {-# SCC "decodeDefault/Fixed" #-}
      ( decodeRefine
         ( \s0 ->
            let s1 = s0 * smult
            in  case Sci.floatingOrInteger s1 of
                  Right ~i
                     | sciIntegerMin <= s1 && s1 <= sciIntegerMax ->
                        Right (MkFixed i)
                     | otherwise -> Left "Integer too large"
                  Left (_ :: Double) -> Left "Lossy conversion"
         )
         decodeDefault
      )
     where
      smult :: Sci.Scientific
      smult = fromInteger (resolution (Proxy @e))

-- | 'S.IntegerColumn', 'S.FloatColumn', 'S.TextColumn'.
instance DecodeDefault Sci.Scientific where
   decodeDefault =
      {-# SCC "decodeDefault/Scientific" #-}
      ( Decode \case
         S.SQLInteger i -> Right (fromIntegral i)
         S.SQLText t -> case scientificFromText t of
            Just x -> Right x
            Nothing -> Left $ errDecodeFailString "Malformed number"
         S.SQLFloat d
            | isNaN d -> Left $ errDecodeFailString "NaN"
            | isInfinite d -> Left $ errDecodeFailString "Infinity"
            | x <- Sci.fromFloatDigits d
            , d == Sci.toRealFloat x ->
               Right x
            | otherwise ->
               Left $ errDecodeFailString "Lossy conversion"
         c ->
            Left $ ErrDecode_Type (sqlDataColumnType c) do
               [S.IntegerColumn, S.FloatColumn, S.TextColumn]
      )

scientificFromText :: T.Text -> Maybe Sci.Scientific
scientificFromText = hush . AB.parseOnly Aep.scientific . T.encodeUtf8

-- | 'S.TextColumn'. Uses @'URI.parseURIReference', which supports both absolute
-- and relative 'URI.URI's with optional fragment identifiers.
instance DecodeDefault URI.URI where
   decodeDefault =
      decodeRefine (note "Invalid URI" . URI.parseURIReference) decodeDefault

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

-- @'decodeBinary'  =  'decodeBinary'' "Data.Binary".'Bin.get'@
decodeBinary :: (Bin.Binary a) => Decode a
decodeBinary = decodeBinary' Bin.get

-- | 'S.BlobColumn'.
decodeBinary' :: Bin.Get a -> Decode a
decodeBinary' ga = flip decodeRefine (decodeDefault @BL.ByteString) \bl ->
   case Bin.runGetOrFail ga bl of
      Right (_, _, a) -> Right a
      Left (_, _, s) -> Left s

-- | 'S.TextColumn'.
decodeRead :: (Prelude.Read a) => Decode a
decodeRead = decodeRefine readEither (decodeDefault @String)

-- | @'decodeAeson' = 'decodeAeson'' "Data.Aeson".'Ae.parseJSON'@
decodeAeson :: (Ae.FromJSON a) => Decode a
decodeAeson = decodeAeson' Ae.parseJSON

-- | 'S.TextColumn'.
decodeAeson' :: (Ae.Value -> Ae.Parser a) -> Decode a
decodeAeson' p = decodeRefine (Ae.parseEither p) (decodeDefault @Ae.Value)