packages feed

polytropos-hasql-0.1.0.2: test/Main.hs

{-# LANGUAGE DataKinds #-}
module Main (main) where

import Test.Tasty
import Test.Tasty.HUnit
import Data.Bits (Bits)
import Data.Functor.Identity (Identity (..))
import Data.Int (Int16, Int32, Int64)
import GHC.Exts (Any)
import qualified Data.Text as T
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as LBS
import Polytropos
import Polytropos.Core (fieldAt, noInlineUnsafeCo, unRowRep)
import Polytropos.Hasql
import qualified PostgreSQL.Binary.Decoding as B

-- Three heterogeneous columns: the differing wire widths (8/4/2 bytes)
-- make column-order observable in the payload slicing. (No plugin
-- synthesized Eq here — the plugin's literal dictionary index only
-- covers Int/Bool/Word, so equality is asserted field-wise below.)
type MyRec =
  Record '[ "a" ':= Int64, "b" ':= Int32, "c" ':= Int16 ]

-- Postgres binary wire encoding for the fixed-width integrals:
-- int2 = 2 bytes BE, int4 = 4 bytes BE, int8 = 8 bytes BE. These are the
-- exact payload formats hasql's int2/int4/int8 decoders (built on
-- postgresql-binary, its codec backend) consume per column.
wireInt64 :: Int64 -> BS.ByteString
wireInt64 = LBS.toStrict . BB.toLazyByteString . BB.int64BE

wireInt32 :: Int32 -> BS.ByteString
wireInt32 = LBS.toStrict . BB.toLazyByteString . BB.int32BE

wireInt16 :: Int16 -> BS.ByteString
wireInt16 = LBS.toStrict . BB.toLazyByteString . BB.int16BE

wireBig :: [Int64] -> BS.ByteString
wireBig = mconcat . map wireInt64

-- One column of the row, decoded at its own type through the same
-- decoder machinery hasql runs per column. This is the per-column step
-- of the walk 'genRow' composes; full Row execution needs a live server
-- (or hasql's hidden internals, verified in the probe session) so the
-- simulation stops at the column boundary.
decodeColumn :: forall a. (Integral a, Bits a) => BS.ByteString -> Either T.Text a
decodeColumn = B.valueParser (B.int :: B.Value a)

-- Assemble a record from per-column wire payloads, consuming columns
-- strictly in row order — one slot per column, exactly as the walk does.
decodeSim3 :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Either T.Text MyRec
decodeSim3 ba bb bc = do
  a <- decodeColumn @Int64 ba
  b <- decodeColumn @Int32 bb
  c <- decodeColumn @Int16 bc
  pure $ insertField (Field @"a") a
       $ insertField (Field @"b") b
       $ insertField (Field @"c") c
       $ emptyRecord

fieldA, fieldB, fieldC :: MyRec -> Int64
fieldA r = unslot (fieldAt 0 (unRowRep r))
fieldB r = fromIntegral (unslot (fieldAt 1 (unRowRep r)) :: Int32)
fieldC r = fromIntegral (unslot (fieldAt 2 (unRowRep r)) :: Int16)

unslot :: Identity Any -> a
unslot = noInlineUnsafeCo

sizedChunks :: Int -> BS.ByteString -> [BS.ByteString]
sizedChunks n bs
  | BS.null bs = []
  | otherwise  = let (c, rest) = BS.splitAt n bs in c : sizedChunks n rest

main :: IO ()
main = defaultMain $ testGroup "Hasql Integration"
  [ testGroup "Row decoder construction"
      [ testCase "recordRow builds a 3-column decoder" $ do
          let rd = recordRow :: HasqlRow MyRec
          -- Construction is total and pure; the decoder exists.
          rd `seq` return ()
      , testCase "recordRow builds a 20-column decoder (no tuple limit)" $ do
          let rd = recordRow :: HasqlRow (Record RowOf20)
          rd `seq` return ()
      ]
  , testGroup "Simulated column consumption"
      [ testCase "heterogeneous columns decode in row order into a record" $
          case decodeSim3 (wireInt64 1) (wireInt32 2) (wireInt16 3) of
            Right r  -> do
              fieldA r @?= 1
              fieldB r @?= 2
              fieldC r @?= 3
            Left e   -> assertFailure (T.unpack e)
      , testCase "columns map to fields strictly in row order" $
          -- Permuting the column payloads permutes the record fields:
          -- (3,2,1) is a different record from (1,2,3), proving each
          -- column landed in its own slot.
          case decodeSim3 (wireInt64 3) (wireInt64 2) (wireInt64 1) of
            Right r  -> do
              fieldA r @?= 3
              fieldB r @?= 2
              fieldC r @?= 1
            Left e   -> assertFailure (T.unpack e)
      , testCase "20 columns of wire bytes roundtrip" $ do
          let vals = [ fromIntegral n :: Int64 | n <- [1 .. 20 :: Int] ]
          -- Exactly one 8-byte column per field, 20 fields, no tuples.
          case mapM (decodeColumn @Int64) (sizedChunks 8 (wireBig vals)) of
            Right vs -> vs @?= vals
            Left e   -> assertFailure (T.unpack e)
      ]
  ]

-- 20 columns: exercises the walk well past the 62-column tuple limit.
type RowOf20 =
  '[ "c00" ':= Int64, "c01" ':= Int64, "c02" ':= Int64, "c03" ':= Int64
   , "c04" ':= Int64, "c05" ':= Int64, "c06" ':= Int64, "c07" ':= Int64
   , "c08" ':= Int64, "c09" ':= Int64, "c10" ':= Int64, "c11" ':= Int64
   , "c12" ':= Int64, "c13" ':= Int64, "c14" ':= Int64, "c15" ':= Int64
   , "c16" ':= Int64, "c17" ':= Int64, "c18" ':= Int64, "c19" ':= Int64
   ]