valiant-0.1.0.0: src/Valiant/Binary/Unbounded.hs
{-# OPTIONS_GHC -Wno-orphans #-}
-- | Support for PostgreSQL @-infinity@ and @infinity@ timestamp/date values.
--
-- PostgreSQL allows @-infinity@ and @infinity@ as valid values for
-- @timestamp@, @timestamptz@, and @date@ columns. In binary format these
-- are represented as sentinel values:
--
-- * Timestamps: @INT64_MIN@ for @-infinity@, @INT64_MAX@ for @infinity@
-- * Dates: @INT32_MIN@ for @-infinity@, @INT32_MAX@ for @infinity@
--
-- The standard 'UTCTime', 'LocalTime', and 'Day' types cannot represent
-- infinity. This module provides 'Unbounded' as a wrapper:
--
-- @
-- findUser :: Statement Int32 (Int32, Text, Unbounded UTCTime)
-- findUser = queryFile \"users\/find_by_id.sql\"
-- @
module Valiant.Binary.Unbounded
( Unbounded (..)
, finite
, fromFinite
) where
import Control.DeepSeq (NFData (..))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Bits (shiftL, (.|.))
import Data.Int (Int32, Int64)
import Data.Time (Day, LocalTime, UTCTime)
import GHC.Generics (Generic)
import PgWire.Binary.Types (PgDecode (..), PgEncode (..))
import PgWire.Protocol.Oid (oidDate, oidTimestamp, oidTimestamptz)
import Valiant.Binary.Decode ()
import Valiant.Binary.Encode ()
-- | A value that may be @-infinity@, a finite value, or @infinity@.
-- Used for PostgreSQL timestamp and date types that support infinity.
data Unbounded a
= NegInfinity
-- ^ @-infinity@
| Finite !a
-- ^ A regular finite value.
| PosInfinity
-- ^ @infinity@
deriving stock (Show, Eq, Ord, Generic, Functor)
instance NFData a => NFData (Unbounded a)
-- | Wrap a value as 'Finite'.
finite :: a -> Unbounded a
finite = Finite
{-# INLINE finite #-}
-- | Extract the finite value, or 'Nothing' for infinity.
fromFinite :: Unbounded a -> Maybe a
fromFinite (Finite a) = Just a
fromFinite _ = Nothing
{-# INLINE fromFinite #-}
-- Sentinel values used by PostgreSQL binary format.
pgTimestampPosInf :: Int64
pgTimestampPosInf = 0x7FFFFFFFFFFFFFFF
pgTimestampNegInf :: Int64
pgTimestampNegInf = -0x8000000000000000
pgDatePosInf :: Int32
pgDatePosInf = 0x7FFFFFFF
pgDateNegInf :: Int32
pgDateNegInf = -0x80000000
------------------------------------------------------------------------
-- Unbounded UTCTime (timestamptz)
------------------------------------------------------------------------
instance PgEncode (Unbounded UTCTime) where
pgEncode NegInfinity = encodeInt64BE pgTimestampNegInf
pgEncode PosInfinity = encodeInt64BE pgTimestampPosInf
pgEncode (Finite t) = pgEncode t
{-# INLINE pgEncode #-}
pgOid _ = oidTimestamptz
{-# INLINE pgOid #-}
instance PgDecode (Unbounded UTCTime) where
pgDecode bs = do
micros <- decodeRawInt64 bs
if micros == pgTimestampPosInf then Right PosInfinity
else if micros == pgTimestampNegInf then Right NegInfinity
else Finite <$> pgDecode bs
{-# INLINE pgDecode #-}
------------------------------------------------------------------------
-- Unbounded LocalTime (timestamp)
------------------------------------------------------------------------
instance PgEncode (Unbounded LocalTime) where
pgEncode NegInfinity = encodeInt64BE pgTimestampNegInf
pgEncode PosInfinity = encodeInt64BE pgTimestampPosInf
pgEncode (Finite t) = pgEncode t
{-# INLINE pgEncode #-}
pgOid _ = oidTimestamp
{-# INLINE pgOid #-}
instance PgDecode (Unbounded LocalTime) where
pgDecode bs = do
micros <- decodeRawInt64 bs
if micros == pgTimestampPosInf then Right PosInfinity
else if micros == pgTimestampNegInf then Right NegInfinity
else Finite <$> pgDecode bs
{-# INLINE pgDecode #-}
------------------------------------------------------------------------
-- Unbounded Day (date)
------------------------------------------------------------------------
instance PgEncode (Unbounded Day) where
pgEncode NegInfinity = encodeInt32BE pgDateNegInf
pgEncode PosInfinity = encodeInt32BE pgDatePosInf
pgEncode (Finite d) = pgEncode d
{-# INLINE pgEncode #-}
pgOid _ = oidDate
{-# INLINE pgOid #-}
instance PgDecode (Unbounded Day) where
pgDecode bs = do
days <- decodeRawInt32 bs
if days == pgDatePosInf then Right PosInfinity
else if days == pgDateNegInf then Right NegInfinity
else Finite <$> pgDecode bs
{-# INLINE pgDecode #-}
------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------
decodeRawInt64 :: ByteString -> Either String Int64
decodeRawInt64 bs
| BS.length bs /= 8 = Left $ "Unbounded: expected 8 bytes, got " <> show (BS.length bs)
| otherwise =
let b0 = fromIntegral (BS.index bs 0) :: Int64
b1 = fromIntegral (BS.index bs 1) :: Int64
b2 = fromIntegral (BS.index bs 2) :: Int64
b3 = fromIntegral (BS.index bs 3) :: Int64
b4 = fromIntegral (BS.index bs 4) :: Int64
b5 = fromIntegral (BS.index bs 5) :: Int64
b6 = fromIntegral (BS.index bs 6) :: Int64
b7 = fromIntegral (BS.index bs 7) :: Int64
in Right $ b0 `shiftL` 56 .|. b1 `shiftL` 48 .|. b2 `shiftL` 40 .|. b3 `shiftL` 32
.|. b4 `shiftL` 24 .|. b5 `shiftL` 16 .|. b6 `shiftL` 8 .|. b7
decodeRawInt32 :: ByteString -> Either String Int32
decodeRawInt32 bs
| BS.length bs /= 4 = Left $ "Unbounded: expected 4 bytes, got " <> show (BS.length bs)
| otherwise =
let b0 = fromIntegral (BS.index bs 0) :: Int32
b1 = fromIntegral (BS.index bs 1) :: Int32
b2 = fromIntegral (BS.index bs 2) :: Int32
b3 = fromIntegral (BS.index bs 3) :: Int32
in Right $ b0 `shiftL` 24 .|. b1 `shiftL` 16 .|. b2 `shiftL` 8 .|. b3
encodeInt64BE :: Int64 -> ByteString
encodeInt64BE = pgEncode
encodeInt32BE :: Int32 -> ByteString
encodeInt32BE = pgEncode