packages feed

preql-0.6: src/Preql/Wire/Query.hs

-- | Description: Send queries, decode results, look up OID for a known type name

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
module Preql.Wire.Query where

import Preql.FromSql
import Preql.Wire.Errors
import Preql.Wire.Internal
import Preql.Wire.ToSql

import Control.Exception (try)
import Control.Monad
import Control.Monad.Except
import Data.IORef
import GHC.TypeNats
import Preql.Imports
import System.IO.Unsafe (unsafePerformIO)

import Debug.Trace

import qualified Data.HashMap.Strict as HM
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Sized as VS
import qualified Database.PostgreSQL.LibPQ as PQ

-- Cache of OIDs by type name

type TypeCache = IORef (HM.HashMap Text PQ.Oid)

-- | We make the type cache part of the Connection to offer the option of
-- per-Connection (or striped) caches.  It's also reasonable to share a single
-- cache for an entire multi-threaded program; the @IORef@ supports this usage.
data Connection = Connection
  { rawConnection :: !PQ.Connection
  , typeCache :: !TypeCache
  }

connectdbSharedCache :: ByteString -> IO Connection
connectdbSharedCache str = Connection <$> PQ.connectdb str <*> pure globalCache

connectdbNewCache :: ByteString -> IO Connection
connectdbNewCache str = Connection <$> PQ.connectdb str <*> newIORef mempty

finish :: Connection -> IO ()
finish (Connection conn _) = PQ.finish conn

globalCache :: TypeCache
globalCache = unsafePerformIO (newIORef mempty)

-- send queries, receiving results

queryWith :: KnownNat (Width r) =>
  RowEncoder p -> RowDecoder (Width r) r -> Connection ->
  Query (Width r) -> p -> IO (Either QueryError (Vector r))
queryWith enc dec conn@(Connection pqConn cache) (Query q) params = do
  e_result <- execParams enc pqConn q params
  traceEventIO "execParams > decodeVector"
  case e_result of
    Left err   -> return (Left err)
    Right rows -> decodeVector conn dec rows

-- If there is no result, we don't need a Decoder
queryWith_ :: RowEncoder p -> Connection -> Query n -> p -> IO (Either QueryError ())
queryWith_ enc (Connection conn _) (Query q) params = do
    e_result <- execParams enc conn q params
    return (void e_result)

query :: (ToSql p, FromSql r, KnownNat (Width r)) =>
    Connection -> Query (Width r) -> p -> IO (Either QueryError (Vector r))
query = queryWith toSql fromSql

query_ :: ToSql p => Connection -> Query n -> p -> IO (Either QueryError ())
query_ = queryWith_ toSql

execParams :: RowEncoder p -> PQ.Connection -> ByteString -> p -> IO (Either QueryError PQ.Result)
execParams enc conn q params = do
    e_result <- connectionError conn =<< PQ.execParams conn q (runEncoder enc params) PQ.Binary
    case e_result of
        Left err -> return (Left (ConnectionError err))
        Right res -> do
            status <- PQ.resultStatus res
            if status == PQ.CommandOk || status == PQ.TuplesOk
                then return (Right res)
                else do
                    msg <- PQ.resultErrorMessage res
                        <&> maybe (T.pack (show status)) (decodeUtf8With lenientDecode)
                    return (Left (ConnectionError msg))

connectionError :: PQ.Connection -> Maybe a -> IO (Either Text a)
connectionError _conn (Just a) = return (Right a)
connectionError conn Nothing = do
    m_msg <- liftIO $ PQ.errorMessage conn
    case m_msg of
        Just msg -> return (Left (decodeUtf8With lenientDecode msg))
        Nothing  -> return (Left "No error message available")

-- decoding

decodeVector :: KnownNat n =>
  Connection -> RowDecoder n a -> PQ.Result -> IO (Either QueryError (Vector a))
decodeVector conn rd@(RowDecoder pgtypes _parsers) result = do
    mismatches <- fmap (catMaybes . VS.toList) $ for (VS.zip (VS.enumFromN 0) pgtypes) $ \(column@(PQ.Col cint), expected) -> do
        actual <- PQ.ftype result column
        lookupResult <- lookupType conn expected
        let mismatch = do
              m_name <- liftIO $ PQ.fname result column
              let columnName = decodeUtf8With lenientDecode <$> m_name
              return $ Just (TypeMismatch{column = fromIntegral cint, ..})
        case lookupResult of
          Cached oid | actual == oid -> return Nothing
          FromDb oid | actual == oid -> return Nothing
          Cached _ -> do -- recheck DB, in case type changed under us
            e_oid  <- lookupTypeIgnoreCache conn expected
            case e_oid of
              Right oid | actual == oid -> return Nothing
              _ -> mismatch
          _ -> mismatch
    if not (null mismatches)
        then return (Left (PgTypeMismatch mismatches))
        else do
            (PQ.Row ntuples) <- liftIO $ PQ.ntuples result
            ref <- newIORef (DecoderState result 0 0)
            fmap (first DecoderError) . try $
                V.replicateM (fromIntegral ntuples) (decodeRow ref rd result)
  where

lookupType :: Connection -> PgType -> IO LookupResult
lookupType _ (Oid oid _) = return (FromDb oid)
lookupType conn@(Connection _ cacheRef) expected@(TypeName name) = do
  cache <- readIORef cacheRef
  case HM.lookup name cache of
    Just oid -> return (Cached oid)
    Nothing -> lookupTypeIgnoreCache conn expected
      <&> either LookupError FromDb

data LookupResult
  = Cached PQ.Oid
  | FromDb PQ.Oid
  | LookupError QueryError

lookupTypeIgnoreCache :: Connection -> PgType -> IO (Either QueryError PQ.Oid)
lookupTypeIgnoreCache _ (Oid oid _) = return (Right oid)
lookupTypeIgnoreCache conn@(Connection _ cacheRef) (TypeName name) = do
  e_rows <- query conn "SELECT oid FROM pg_type WHERE typname = $1" name
  case fmap (V.!? 0) e_rows of
    Left e -> return (Left e)
    Right (Just oid) -> do
      atomicModifyIORef cacheRef (\cache -> (HM.insert name oid cache, ()))
      return (Right oid)
    Right Nothing -> return (Left (ConnectionError ("No oid for: " <> name)))

-- transactions

data IsolationLevel = ReadCommitted
    | RepeatableRead
    | Serializable
    deriving (Show, Read, Eq, Ord, Enum, Bounded)

begin :: Connection -> IsolationLevel -> IO (Either QueryError ())
begin conn level = query_ conn q () where
  q = case level of
    ReadCommitted  -> "BEGIN ISOLATION LEVEL READ COMMITTED"
    RepeatableRead -> "BEGIN ISOLATION LEVEL REPEATABLE READ"
    Serializable   -> "BEGIN ISOLATION LEVEL SERIALIZABLE"

commit :: Connection -> IO (Either QueryError ())
commit conn = query_ conn "COMMIT" ()

rollback :: Connection -> IO (Either QueryError ())
rollback conn = query_ conn "ROLLBACK" ()