{- | Copyright : Flipstone Technology Partners 2023 License : MIT Stability : Stable The functions in this module are named with the intent that it is imported qualified as 'SqlValue'. @since 1.0.0.0 -} module Orville.PostgreSQL.Raw.SqlValue ( SqlValue , isSqlNull , sqlNull , fromInt8 , toInt8 , fromInt16 , toInt16 , fromInt32 , toInt32 , fromInt64 , toInt64 , fromInt , toInt , fromWord8 , toWord8 , fromWord16 , toWord16 , fromWord32 , toWord32 , fromWord64 , toWord64 , fromWord , toWord , fromDouble , toDouble , fromBool , toBool , fromText , toText , fromDay , toDay , fromUTCTime , toUTCTime , fromLocalTime , toLocalTime , fromRawBytes , fromRawBytesNullable , toPgValue ) where import qualified Control.Exception as Exc import qualified Data.Attoparsec.ByteString as AttoBS import qualified Data.Attoparsec.ByteString.Char8 as AttoB8 import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Lazy as LBS import Data.Int (Int16, Int32, Int64, Int8) import qualified Data.Text as T import qualified Data.Text.Encoding as TextEnc import qualified Data.Time as Time import qualified Data.Typeable as Typeable import Data.Word (Word16, Word32, Word64, Word8) import Orville.PostgreSQL.Raw.PgTextFormatValue (PgTextFormatValue) import qualified Orville.PostgreSQL.Raw.PgTextFormatValue as PgTextFormatValue import qualified Orville.PostgreSQL.Raw.PgTime as PgTime {- | 'SqlValue' represents a value that is in encoded format for use with LibPQ. It is used both for values passed to LibPQ and values parsed from LibPQ. The conversion functions in "Orville.PostgreSQL.Raw.SqlValue" can be used to convert to and from the value. @since 1.0.0.0 -} data SqlValue = SqlValue PgTextFormatValue | SqlNull deriving (Eq) {- | Checks whether the 'SqlValue' represents a SQL NULL value in the database. @since 1.0.0.0 -} isSqlNull :: SqlValue -> Bool isSqlNull sqlValue = case sqlValue of SqlValue _ -> False SqlNull -> True {- | A value of 'SqlValue' that will be interpreted as a SQL NULL value when passed to the database. @since 1.0.0.0 -} sqlNull :: SqlValue sqlNull = SqlNull {- | Converts a 'SqlValue' to its underlying raw bytes as it will be represented when sent to the database. The output should be recognizable as similar to values you would write in a query. If the value represents a SQL NULL value, 'Nothing' is returned. @since 1.0.0.0 -} toPgValue :: SqlValue -> Maybe PgTextFormatValue toPgValue sqlValue = case sqlValue of SqlValue value -> Just value SqlNull -> Nothing {- | Creates a 'SqlValue' from a raw bytestring as if the bytes had been returned by the database. This function does not interpret the bytes in any way, but using decode functions on them might fail depending on whether the bytes can be parsed as the requested type. Note: A value to represent a SQL NULL cannot be constructed using this function. See 'fromRawBytesNullable' for how to represent a nullable raw value. @since 1.0.0.0 -} fromRawBytes :: BS.ByteString -> SqlValue fromRawBytes = SqlValue . PgTextFormatValue.fromByteString {- | Creates a 'SqlValue' from a raw bytestring. If 'Nothing' is specified as the input parameter then the resulting 'SqlValue' will represent a NULL value in SQL. Otherwise, the bytes given are used in the same way as 'fromRawBytes'. @since 1.0.0.0 -} fromRawBytesNullable :: Maybe BS.ByteString -> SqlValue fromRawBytesNullable = maybe sqlNull fromRawBytes {- | Encodes an 'Int8' value for use with the database. @since 1.0.0.0 -} fromInt8 :: Int8 -> SqlValue fromInt8 = fromBSBuilderWithNoNULs BSB.int8Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Int8' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toInt8 :: SqlValue -> Either String Int8 toInt8 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes an 'Int16' value for use with the database. @since 1.0.0.0 -} fromInt16 :: Int16 -> SqlValue fromInt16 = fromBSBuilderWithNoNULs BSB.int16Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Int16' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toInt16 :: SqlValue -> Either String Int16 toInt16 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes an 'Int32' value for use with the database. @since 1.0.0.0 -} fromInt32 :: Int32 -> SqlValue fromInt32 = fromBSBuilderWithNoNULs BSB.int32Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Int32' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toInt32 :: SqlValue -> Either String Int32 toInt32 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes an 'Int64' value for use with the database. @since 1.0.0.0 -} fromInt64 :: Int64 -> SqlValue fromInt64 = fromBSBuilderWithNoNULs BSB.int64Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Int' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toInt64 :: SqlValue -> Either String Int64 toInt64 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes an 'Int' value for use with the database. @since 1.0.0.0 -} fromInt :: Int -> SqlValue fromInt = fromBSBuilderWithNoNULs BSB.intDec {- | Attempts to decode a 'SqlValue' as a Haskell 'Int' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toInt :: SqlValue -> Either String Int toInt = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes a 'Word8' value for use with the database. @since 1.0.0.0 -} fromWord8 :: Word8 -> SqlValue fromWord8 = fromBSBuilderWithNoNULs BSB.word8Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Word8' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toWord8 :: SqlValue -> Either String Word8 toWord8 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes a 'Word16' value for use with the database. @since 1.0.0.0 -} fromWord16 :: Word16 -> SqlValue fromWord16 = fromBSBuilderWithNoNULs BSB.word16Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Word16' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toWord16 :: SqlValue -> Either String Word16 toWord16 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes a 'Word32' value for use with the database. @since 1.0.0.0 -} fromWord32 :: Word32 -> SqlValue fromWord32 = fromBSBuilderWithNoNULs BSB.word32Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Word32' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toWord32 :: SqlValue -> Either String Word32 toWord32 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes a 'Word64' value for use with the database. @since 1.0.0.0 -} fromWord64 :: Word64 -> SqlValue fromWord64 = fromBSBuilderWithNoNULs BSB.word64Dec {- | Attempts to decode a 'SqlValue' as a Haskell 'Word64' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toWord64 :: SqlValue -> Either String Word64 toWord64 = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes a 'Word' value for use with the database. @since 1.0.0.0 -} fromWord :: Word -> SqlValue fromWord = fromBSBuilderWithNoNULs BSB.wordDec {- | Attempts to decode a 'SqlValue' as a Haskell 'Word' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toWord :: SqlValue -> Either String Word toWord = toParsedValue (AttoB8.signed AttoB8.decimal) {- | Encodes a 'Double' value for use with the database. @since 1.0.0.0 -} fromDouble :: Double -> SqlValue fromDouble = fromBSBuilderWithNoNULs BSB.doubleDec {- | Attempts to decode a 'SqlValue' as a Haskell 'Double' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toDouble :: SqlValue -> Either String Double toDouble = toParsedValue (AttoB8.signed AttoB8.double) {- | Encodes a 'Bool' value for use with the database. @since 1.0.0.0 -} fromBool :: Bool -> SqlValue fromBool = fromBSBuilderWithNoNULs $ \bool -> case bool of True -> BSB.char8 't' False -> BSB.char8 'f' {- | Attempts to decode a 'SqlValue' as a Haskell 'Bool' value. If decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toBool :: SqlValue -> Either String Bool toBool = toParsedValue $ do char <- AttoB8.anyChar case char of 't' -> pure True 'f' -> pure False _ -> fail "Invalid boolean character value" {- | Encodes a 'T.Text' value as UTF-8 so that it can be used with the database. @since 1.0.0.0 -} fromText :: T.Text -> SqlValue fromText = SqlValue . PgTextFormatValue.fromByteString . TextEnc.encodeUtf8 {- | Attempts to decode a 'SqlValue' as UTF-8 text. If the decoding fails, 'Nothing' is returned. Note: This decoding _only_ fails if the bytes returned from the database are not a valid UTF-8 sequence of bytes. Otherwise it always succeeds. @since 1.0.0.0 -} toText :: SqlValue -> Either String T.Text toText = toBytesValue $ \bytes -> case TextEnc.decodeUtf8' bytes of Right t -> Right t Left err -> Left $ Exc.displayException err {- | Encodes a 'Time.Day' value as text in YYYY-MM-DD format so that it can be used with the database. @since 1.0.0.0 -} fromDay :: Time.Day -> SqlValue fromDay = SqlValue . PgTextFormatValue.unsafeFromByteString . PgTime.dayToPostgreSQL {- | Attempts to decode a 'SqlValue' as into a 'Time.Day' value by parsing it from YYYY-MM-DD format. If the decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toDay :: SqlValue -> Either String Time.Day toDay = toParsedValue PgTime.day {- | Encodes a 'Time.UTCTime' in ISO-8601 format for use with the database. @since 1.0.0.0 -} fromUTCTime :: Time.UTCTime -> SqlValue fromUTCTime = SqlValue . PgTextFormatValue.unsafeFromByteString . PgTime.utcTimeToPostgreSQL {- | Encodes a 'Time.LocalTime' in ISO-8601 format for use with the database. @since 1.0.0.0 -} fromLocalTime :: Time.LocalTime -> SqlValue fromLocalTime = SqlValue . PgTextFormatValue.unsafeFromByteString . PgTime.localTimeToPostgreSQL {- | Attempts to decode a 'SqlValue' as a 'Time.LocalTime' formatted in ISO-8601 format in the default locale. If the decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toLocalTime :: SqlValue -> Either String Time.LocalTime toLocalTime = toParsedValue PgTime.localTime {- | Attempts to decode a 'SqlValue' as a 'Time.UTCTime' formatted in ISO-8601 format with time zone. If the decoding fails, 'Nothing' is returned. @since 1.0.0.0 -} toUTCTime :: SqlValue -> Either String Time.UTCTime toUTCTime = toParsedValue PgTime.utcTime {- | An internal helper function that constructs a 'SqlValue' via a bytestring 'BS8.Builder'. @since 1.0.0.0 -} fromBSBuilderWithNoNULs :: (a -> BSB.Builder) -> a -> SqlValue fromBSBuilderWithNoNULs builder = SqlValue . PgTextFormatValue.unsafeFromByteString . LBS.toStrict . BSB.toLazyByteString . builder {- | An internal helper function that parses 'SqlValue' via an Attoparsec parser. @since 1.0.0.0 -} toParsedValue :: Typeable.Typeable a => AttoB8.Parser a -> SqlValue -> Either String a toParsedValue parser = toBytesValue (AttoBS.parseOnly parser) {- | An internal helper function that parses the bytes from a 'SqlValue' with the given parsing function. If the 'SqlValue' is NULL, this function returns an error an a 'Left' value. If the parsing function fails (by returning 'Left'), the error it returns in returned by this function. @since 1.0.0.0 -} toBytesValue :: Typeable.Typeable a => (BS.ByteString -> Either String a) -> SqlValue -> Either String a toBytesValue byteParser sqlValue = let result = case sqlValue of SqlNull -> Left "Unexpected SQL NULL value" SqlValue bytes -> byteParser (PgTextFormatValue.toByteString bytes) typeRepOfA = -- result is the 'proxy a' for typeRep Typeable.typeRep result in case result of Right _ -> result Left err -> Left $ "Failed to decode PostgreSQL value as " <> Typeable.showsTypeRep typeRepOfA (": " <> err)