packages feed

preql-0.6: src/Preql/FromSql/Tuple.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Description: newtype to support FromSql instances for row types.

module Preql.FromSql.Tuple where

import Preql.FromSql.Class (FieldDecoder(..))
import Preql.Wire.Errors (PgType(..))

import Control.Monad (unless)
import Data.Bits ((.|.), Bits, shiftL)
import Data.Int (Int32)
import qualified BinaryParser as BP
import qualified Data.ByteString as BS
import qualified Data.Text as T
import qualified Database.PostgreSQL.LibPQ as PQ

-- | Wrapper for Postgres anonymous row types (sometimes called record
-- types), so instance resolution picks the right decoder.  The useful
-- instances are for (Haskell) tuples.  Postgres allows row types with
-- a single field, but the instances would overlap with those for
-- nested row types, so we do not provide them.
newtype Tuple r = Tuple r
  deriving (Eq, Ord, Show)

-- Unlike the same-named functions in PostgreSQL.Binary.Decoding, these check
-- the number of components and the OIDs of each component.

-- | Helper for decoding composites
newtype Composite a = Composite (BP.BinaryParser a)
  deriving newtype (Functor, Applicative)

composite :: Int -> Composite a -> BP.BinaryParser a
composite n (Composite parser) = do
  size <- intOfSize 4
  unless (size == n) (BP.failure "composite has wrong size")
  parser

valueComposite :: FieldDecoder a -> Composite a
valueComposite (FieldDecoder pgType parser) = Composite $ do
  case pgType of
    -- For now, we only confirm statically-known OIDs.  To do more, we'll need
    -- to feed through the OID cache (not yet implemented) and the Connection
    -- (which will be a problem if we ever implement lazy decoding)
    -- Statically-known OIDs seems like a reasonable compromise for now, until
    -- other parts of the design stabilize.
    Oid (PQ.Oid expected) _ -> do
      actual <- intOfSize 4
      unless (actual == expected) (BP.failure $ "OID in composite expected=" <> showt expected <> " actual=" <> showt actual)
    TypeName _ -> BP.unitOfSize 4 -- TODO check cache, maybe query DB
  onContent parser >>=
    maybe (BP.failure "unexpected null") pure
  where showt = T.pack . show

{-# INLINE intOfSize #-}
intOfSize :: (Integral a, Bits a) => Int -> BP.BinaryParser a
intOfSize x =
  fmap (BS.foldl' (\n h -> shiftL n 8 .|. fromIntegral h) 0) (BP.bytesOfSize x)

{-# INLINABLE onContent #-}
onContent :: BP.BinaryParser a -> BP.BinaryParser ( Maybe a )
onContent decoder = do
  size :: Int32 <- intOfSize 4
  case size of
    (-1) -> pure Nothing
    n -> fmap Just (BP.sized (fromIntegral n) decoder)