valiant-0.1.0.0: src/Valiant/Binary/Interval.hs
{-# OPTIONS_GHC -Wno-orphans #-}
-- | Binary encode/decode for PostgreSQL @interval@ type.
--
-- PG stores interval as:
-- 8 bytes: microseconds (Int64)
-- 4 bytes: days (Int32)
-- 4 bytes: months (Int32)
-- Total: 16 bytes, big-endian.
module Valiant.Binary.Interval
( PgInterval (..)
) where
import Control.DeepSeq (NFData)
import Data.Bits (shiftL, (.|.))
import Data.ByteString (ByteString)
import GHC.Generics (Generic)
import Data.ByteString qualified as BS
import Data.ByteString.Builder qualified as B
import Data.ByteString.Lazy qualified as LBS
import Data.Int (Int32, Int64)
import PgWire.Binary.Types (PgDecode (..), PgEncode (..))
import PgWire.Protocol.Oid (oidInterval)
-- | A PostgreSQL interval value.
data PgInterval = PgInterval
{ intervalMicroseconds :: {-# UNPACK #-} !Int64
, intervalDays :: {-# UNPACK #-} !Int32
, intervalMonths :: {-# UNPACK #-} !Int32
}
deriving stock (Show, Eq, Ord, Generic)
instance NFData PgInterval
instance PgEncode PgInterval where
pgEncode (PgInterval micros days months) =
LBS.toStrict . B.toLazyByteString $
B.int64BE micros
<> B.int32BE days
<> B.int32BE months
pgOid _ = oidInterval
instance PgDecode PgInterval where
pgDecode bs
| BS.length bs /= 16 = Left $ "interval: expected 16 bytes, got " <> show (BS.length bs)
| otherwise = do
micros <- decodeInt64BE bs 0
days <- decodeInt32BE bs 8
months <- decodeInt32BE bs 12
Right (PgInterval micros days months)
-- Helpers -------------------------------------------------------------------
decodeInt32BE :: ByteString -> Int -> Either String Int32
decodeInt32BE bs off
| off + 4 > BS.length bs = Left "interval: int32 out of bounds"
| otherwise =
let b0 = fromIntegral (BS.index bs off) :: Int32
b1 = fromIntegral (BS.index bs (off + 1)) :: Int32
b2 = fromIntegral (BS.index bs (off + 2)) :: Int32
b3 = fromIntegral (BS.index bs (off + 3)) :: Int32
in Right (b0 `shiftL` 24 .|. b1 `shiftL` 16 .|. b2 `shiftL` 8 .|. b3)
decodeInt64BE :: ByteString -> Int -> Either String Int64
decodeInt64BE bs off
| off + 8 > BS.length bs = Left "interval: int64 out of bounds"
| otherwise =
let go !acc !i
| i >= 8 = acc
| otherwise = go (acc `shiftL` 8 .|. fromIntegral (BS.index bs (off + i))) (i + 1)
in Right (go 0 0)