valiant-0.1.0.0: src/Valiant/Binary/Encode.hs
{-# OPTIONS_GHC -Wno-orphans #-}
module Valiant.Binary.Encode
() where
import Data.Bits (unsafeShiftR)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Internal (unsafeCreate)
import Data.Int (Int16, Int32, Int64)
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Data.Time
( Day
, LocalTime (..)
, TimeOfDay (..)
, TimeZone (..)
, UTCTime (..)
, ZonedTime (..)
, diffTimeToPicoseconds
, fromGregorian
, timeOfDayToTime
, toModifiedJulianDay
, zonedTimeToUTC
)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Data.Word (Word8, Word32, Word64)
import Foreign.Ptr (Ptr)
import Foreign.Storable (pokeByteOff)
import GHC.Float (castFloatToWord32, castDoubleToWord64)
import PgWire.Binary.Types (PgEncode (..))
import PgWire.Protocol.Oid
-- PG epoch: 2000-01-01 00:00:00 UTC
pgEpochOffsetMicros :: Int64
pgEpochOffsetMicros = 946684800000000
{-# INLINE pgEpochOffsetMicros #-}
pgEpochDay :: Integer
pgEpochDay = toModifiedJulianDay (fromGregorian 2000 1 1)
{-# INLINE pgEpochDay #-}
-- Direct byte writes --------------------------------------------------------
-- These avoid Builder → LazyByteString → toStrict overhead for fixed-size
-- types. A single unsafeCreate + poke is ~3x faster than the Builder path
-- for 2-8 byte values.
int16BE :: Int16 -> ByteString
int16BE n = unsafeCreate 2 $ \p -> pokeInt16BE p 0 n
{-# INLINE int16BE #-}
int32BE :: Int32 -> ByteString
int32BE n = unsafeCreate 4 $ \p -> pokeInt32BE p 0 n
{-# INLINE int32BE #-}
int64BE :: Int64 -> ByteString
int64BE n = unsafeCreate 8 $ \p -> pokeInt64BE p 0 n
{-# INLINE int64BE #-}
floatBE :: Float -> ByteString
floatBE f = let !w = castFloatToWord32 f in unsafeCreate 4 $ \p -> pokeWord32BE p 0 w
{-# INLINE floatBE #-}
doubleBE :: Double -> ByteString
doubleBE d = let !w = castDoubleToWord64 d in unsafeCreate 8 $ \p -> pokeWord64BE p 0 w
{-# INLINE doubleBE #-}
-- Poke helpers — write big-endian at an offset into a Ptr
pokeInt16BE :: Ptr Word8 -> Int -> Int16 -> IO ()
pokeInt16BE p off n = do
pokeByteOff p off (fromIntegral (n `unsafeShiftR` 8) :: Word8)
pokeByteOff p (off + 1) (fromIntegral n :: Word8)
{-# INLINE pokeInt16BE #-}
pokeInt32BE :: Ptr Word8 -> Int -> Int32 -> IO ()
pokeInt32BE p off n = do
pokeByteOff p off (fromIntegral (n `unsafeShiftR` 24) :: Word8)
pokeByteOff p (off + 1) (fromIntegral (n `unsafeShiftR` 16) :: Word8)
pokeByteOff p (off + 2) (fromIntegral (n `unsafeShiftR` 8) :: Word8)
pokeByteOff p (off + 3) (fromIntegral n :: Word8)
{-# INLINE pokeInt32BE #-}
pokeInt64BE :: Ptr Word8 -> Int -> Int64 -> IO ()
pokeInt64BE p off n = do
pokeByteOff p off (fromIntegral (n `unsafeShiftR` 56) :: Word8)
pokeByteOff p (off + 1) (fromIntegral (n `unsafeShiftR` 48) :: Word8)
pokeByteOff p (off + 2) (fromIntegral (n `unsafeShiftR` 40) :: Word8)
pokeByteOff p (off + 3) (fromIntegral (n `unsafeShiftR` 32) :: Word8)
pokeByteOff p (off + 4) (fromIntegral (n `unsafeShiftR` 24) :: Word8)
pokeByteOff p (off + 5) (fromIntegral (n `unsafeShiftR` 16) :: Word8)
pokeByteOff p (off + 6) (fromIntegral (n `unsafeShiftR` 8) :: Word8)
pokeByteOff p (off + 7) (fromIntegral n :: Word8)
{-# INLINE pokeInt64BE #-}
pokeWord32BE :: Ptr Word8 -> Int -> Word32 -> IO ()
pokeWord32BE p off n = do
pokeByteOff p off (fromIntegral (n `unsafeShiftR` 24) :: Word8)
pokeByteOff p (off + 1) (fromIntegral (n `unsafeShiftR` 16) :: Word8)
pokeByteOff p (off + 2) (fromIntegral (n `unsafeShiftR` 8) :: Word8)
pokeByteOff p (off + 3) (fromIntegral n :: Word8)
{-# INLINE pokeWord32BE #-}
pokeWord64BE :: Ptr Word8 -> Int -> Word64 -> IO ()
pokeWord64BE p off n = do
pokeByteOff p off (fromIntegral (n `unsafeShiftR` 56) :: Word8)
pokeByteOff p (off + 1) (fromIntegral (n `unsafeShiftR` 48) :: Word8)
pokeByteOff p (off + 2) (fromIntegral (n `unsafeShiftR` 40) :: Word8)
pokeByteOff p (off + 3) (fromIntegral (n `unsafeShiftR` 32) :: Word8)
pokeByteOff p (off + 4) (fromIntegral (n `unsafeShiftR` 24) :: Word8)
pokeByteOff p (off + 5) (fromIntegral (n `unsafeShiftR` 16) :: Word8)
pokeByteOff p (off + 6) (fromIntegral (n `unsafeShiftR` 8) :: Word8)
pokeByteOff p (off + 7) (fromIntegral n :: Word8)
{-# INLINE pokeWord64BE #-}
-- Instances ---------------------------------------------------------------
instance PgEncode Bool where
pgEncode True = BS.singleton 1
pgEncode False = BS.singleton 0
{-# INLINE pgEncode #-}
pgOid _ = oidBool
{-# INLINE pgOid #-}
instance PgEncode Int16 where
pgEncode = int16BE
{-# INLINE pgEncode #-}
pgOid _ = oidInt2
{-# INLINE pgOid #-}
instance PgEncode Int32 where
pgEncode = int32BE
{-# INLINE pgEncode #-}
pgOid _ = oidInt4
{-# INLINE pgOid #-}
instance PgEncode Int64 where
pgEncode = int64BE
{-# INLINE pgEncode #-}
pgOid _ = oidInt8
{-# INLINE pgOid #-}
instance PgEncode Float where
pgEncode = floatBE
{-# INLINE pgEncode #-}
pgOid _ = oidFloat4
{-# INLINE pgOid #-}
instance PgEncode Double where
pgEncode = doubleBE
{-# INLINE pgEncode #-}
pgOid _ = oidFloat8
{-# INLINE pgOid #-}
instance PgEncode Text where
pgEncode = TE.encodeUtf8
{-# INLINE pgEncode #-}
pgOid _ = oidText
{-# INLINE pgOid #-}
instance PgEncode ByteString where
pgEncode = id
{-# INLINE pgEncode #-}
pgOid _ = oidBytea
{-# INLINE pgOid #-}
instance PgEncode UTCTime where
pgEncode t =
let !unixMicros = round (utcTimeToPOSIXSeconds t * 1000000) :: Int64
!pgMicros = unixMicros - pgEpochOffsetMicros
in int64BE pgMicros
{-# INLINE pgEncode #-}
pgOid _ = oidTimestamptz
{-# INLINE pgOid #-}
instance PgEncode Day where
pgEncode d =
let !daysSincePgEpoch = toModifiedJulianDay d - pgEpochDay
in int32BE (fromIntegral daysSincePgEpoch)
{-# INLINE pgEncode #-}
pgOid _ = oidDate
{-# INLINE pgOid #-}
instance PgEncode TimeOfDay where
pgEncode tod =
let !picos = diffTimeToPicoseconds (timeOfDayToTime tod)
!micros = picos `div` 1000000
in int64BE (fromIntegral micros)
{-# INLINE pgEncode #-}
pgOid _ = oidTime
{-# INLINE pgOid #-}
instance PgEncode LocalTime where
pgEncode (LocalTime d tod) =
let !daysSincePgEpoch = toModifiedJulianDay d - pgEpochDay
!picos = diffTimeToPicoseconds (timeOfDayToTime tod)
!microsSinceMidnight = picos `div` 1000000
!totalMicros = fromIntegral daysSincePgEpoch * 86400000000 + fromIntegral microsSinceMidnight
in int64BE totalMicros
{-# INLINE pgEncode #-}
pgOid _ = oidTimestamp
{-# INLINE pgOid #-}
instance PgEncode ZonedTime where
pgEncode = pgEncode . zonedTimeToUTC
{-# INLINE pgEncode #-}
pgOid _ = oidTimestamptz
{-# INLINE pgOid #-}
-- | @timetz@ binary format: 8 bytes (microseconds) + 4 bytes (UTC offset in seconds, negated).
-- PostgreSQL stores the offset as seconds *west* of UTC (negated from the usual convention).
instance PgEncode (TimeOfDay, TimeZone) where
pgEncode (tod, tz) =
let !picos = diffTimeToPicoseconds (timeOfDayToTime tod)
!micros = picos `div` 1000000
!offsetSecs = fromIntegral (negate (timeZoneMinutes tz * 60)) :: Int32
in unsafeCreate 12 $ \p -> do
pokeInt64BE p 0 (fromIntegral micros)
pokeInt32BE p 8 offsetSecs
{-# INLINE pgEncode #-}
pgOid _ = oidTimetz
{-# INLINE pgOid #-}