packages feed

valiant-0.1.0.0: src/Valiant/FromRowFast.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Fast row decoding that throws exceptions instead of returning Either.
--
-- 'FromRowFast' eliminates the per-column @Right@ wrapper allocation
-- that 'FromRow' requires. On wide rows (10+ columns), this can be
-- 10-20% faster because:
--
-- 1. No @Either String a@ allocation per column decode
-- 2. No @<*>@ chain threading the @Either@ through Applicative
-- 3. Direct constructor application without wrapping
--
-- The trade-off: decode errors become imprecise exceptions instead of
-- structured @Left@ values. Since decode errors indicate a bug (schema
-- mismatch), not a runtime condition, this is acceptable for most code.
--
-- @
-- -- Use fetchAllFast instead of fetchAll for hot paths:
-- users <- fetchAllFast conn listAllUsers ()
-- @
module Valiant.FromRowFast
  ( FromRowFast (..)
  , DecodeColumnFast (..)
  ) where

import Data.ByteString (ByteString)
import Data.Kind (Type)
import Data.Vector (Vector)
import Data.Vector qualified as V
import GHC.Generics
import Valiant.Binary.Decode ()
import PgWire.Binary.Types (PgDecode (..))

-- | Closed type family: is this type @Maybe a@?
type family Nullable (a :: Type) :: Bool where
  Nullable (Maybe _) = 'True
  Nullable _         = 'False

-- | Decode a single column, throwing on errors instead of returning Either.
-- No bounds checking — the DataRow parser already validated column count.
class DecodeColumnFast a where
  decodeColumnFast :: Vector (Maybe ByteString) -> Int -> a

instance (Nullable a ~ flag, DecodeColumnFastImpl flag a) => DecodeColumnFast a where
  decodeColumnFast = decodeColumnFastImpl @flag
  {-# INLINE decodeColumnFast #-}

class DecodeColumnFastImpl (flag :: Bool) a where
  decodeColumnFastImpl :: Vector (Maybe ByteString) -> Int -> a

-- Non-nullable: NULL throws, decode errors throw.
instance (PgDecode a) => DecodeColumnFastImpl 'False a where
  decodeColumnFastImpl row idx =
    case V.unsafeIndex row idx of
      Nothing -> error $ "Column " <> show idx <> " is NULL"
      Just bs -> case pgDecode bs of
        Left err -> error err
        Right !val -> val
  {-# INLINE decodeColumnFastImpl #-}

-- Nullable: NULL → Nothing, decode errors throw.
instance (PgDecode a) => DecodeColumnFastImpl 'True (Maybe a) where
  decodeColumnFastImpl row idx =
    case V.unsafeIndex row idx of
      Nothing -> Nothing
      Just bs -> case pgDecode bs of
        Left err -> error err
        Right !val -> Just val
  {-# INLINE decodeColumnFastImpl #-}

-- | Fast row decoder. Throws on errors instead of returning Either.
--
-- Derive via @Generic@:
--
-- @
-- data User = User
--   { userId :: Int32, userName :: Text, userEmail :: Maybe Text }
--   deriving stock (Generic)
--   deriving anyclass (FromRowFast)
-- @
class FromRowFast a where
  fromRowFast :: Vector (Maybe ByteString) -> a
  default fromRowFast :: (Generic a, GFromRowFast (Rep a)) => Vector (Maybe ByteString) -> a
  fromRowFast row = to (gFromRowFast row 0)

-- Generic machinery (same structure as FromRow but without Either)
class GFromRowFast f where
  gFromRowFast :: Vector (Maybe ByteString) -> Int -> f p
  gFieldCountFast :: proxy f -> Int

instance (GFromRowFast f) => GFromRowFast (M1 D c f) where
  gFromRowFast row idx = M1 (gFromRowFast row idx)
  gFieldCountFast _ = gFieldCountFast (undefined :: proxy f)
  {-# INLINE gFromRowFast #-}
  {-# INLINE gFieldCountFast #-}

instance (GFromRowFast f) => GFromRowFast (M1 C c f) where
  gFromRowFast row idx = M1 (gFromRowFast row idx)
  gFieldCountFast _ = gFieldCountFast (undefined :: proxy f)
  {-# INLINE gFromRowFast #-}
  {-# INLINE gFieldCountFast #-}

instance (GFromRowFast f) => GFromRowFast (M1 S c f) where
  gFromRowFast row idx = M1 (gFromRowFast row idx)
  gFieldCountFast _ = gFieldCountFast (undefined :: proxy f)
  {-# INLINE gFromRowFast #-}
  {-# INLINE gFieldCountFast #-}

instance (DecodeColumnFast a) => GFromRowFast (K1 R a) where
  gFromRowFast row idx = K1 (decodeColumnFast row idx)
  gFieldCountFast _ = 1
  {-# INLINE gFromRowFast #-}
  {-# INLINE gFieldCountFast #-}

instance (GFromRowFast f, GFromRowFast g) => GFromRowFast (f :*: g) where
  gFromRowFast row idx =
    let !l = gFromRowFast row idx
        !r = gFromRowFast row (idx + gFieldCountFast (undefined :: proxy f))
     in l :*: r
  gFieldCountFast _ = gFieldCountFast (undefined :: proxy f) + gFieldCountFast (undefined :: proxy g)
  {-# INLINE gFromRowFast #-}
  {-# INLINE gFieldCountFast #-}

instance GFromRowFast U1 where
  gFromRowFast _ _ = U1
  gFieldCountFast _ = 0
  {-# INLINE gFromRowFast #-}
  {-# INLINE gFieldCountFast #-}

-- ── Instances ──────────────────────────────────────────────────────

instance {-# OVERLAPPING #-} FromRowFast () where
  fromRowFast _ = ()

instance (DecodeColumnFast a) => FromRowFast a where
  fromRowFast row = decodeColumnFast row 0

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b) => FromRowFast (a, b) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c) => FromRowFast (a, b, c) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d) => FromRowFast (a, b, c, d) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e) => FromRowFast (a, b, c, d, e) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f) => FromRowFast (a, b, c, d, e, f) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f, DecodeColumnFast g) => FromRowFast (a, b, c, d, e, f, g) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5, decodeColumnFast row 6)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f, DecodeColumnFast g, DecodeColumnFast h) => FromRowFast (a, b, c, d, e, f, g, h) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5, decodeColumnFast row 6, decodeColumnFast row 7)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f, DecodeColumnFast g, DecodeColumnFast h, DecodeColumnFast i) => FromRowFast (a, b, c, d, e, f, g, h, i) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5, decodeColumnFast row 6, decodeColumnFast row 7, decodeColumnFast row 8)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f, DecodeColumnFast g, DecodeColumnFast h, DecodeColumnFast i, DecodeColumnFast j) => FromRowFast (a, b, c, d, e, f, g, h, i, j) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5, decodeColumnFast row 6, decodeColumnFast row 7, decodeColumnFast row 8, decodeColumnFast row 9)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f, DecodeColumnFast g, DecodeColumnFast h, DecodeColumnFast i, DecodeColumnFast j, DecodeColumnFast k) => FromRowFast (a, b, c, d, e, f, g, h, i, j, k) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5, decodeColumnFast row 6, decodeColumnFast row 7, decodeColumnFast row 8, decodeColumnFast row 9, decodeColumnFast row 10)

instance {-# OVERLAPPING #-} (DecodeColumnFast a, DecodeColumnFast b, DecodeColumnFast c, DecodeColumnFast d, DecodeColumnFast e, DecodeColumnFast f, DecodeColumnFast g, DecodeColumnFast h, DecodeColumnFast i, DecodeColumnFast j, DecodeColumnFast k, DecodeColumnFast l) => FromRowFast (a, b, c, d, e, f, g, h, i, j, k, l) where
  fromRowFast row = (decodeColumnFast row 0, decodeColumnFast row 1, decodeColumnFast row 2, decodeColumnFast row 3, decodeColumnFast row 4, decodeColumnFast row 5, decodeColumnFast row 6, decodeColumnFast row 7, decodeColumnFast row 8, decodeColumnFast row 9, decodeColumnFast row 10, decodeColumnFast row 11)