packages feed

HDBC-odbc-2.6.0.0: Database/HDBC/ODBC/Wrappers.hs

{-| Defines haskell wrappers over native ODBC objects to free
    you from some memory (and threading) management.
-}
{-# LANGUAGE OverloadedStrings #-}
module Database.HDBC.ODBC.Wrappers
  ( EnvWrapper (..)
  , sqlAllocEnv
  , withMaybeEnv
  , withEnvOrDie
  , freeEnvIfNotAlready
  , DbcWrapper (..)
  , sqlAllocDbc
  , withMaybeDbc
  , withDbcOrDie
  , tryDisconnectAndFree
  , freeDbcIfNotAlready
  , StmtWrapper (..)
  , sqlAllocStmt
  , withMaybeStmt
  , withStmtOrDie
  , freeStmtIfNotAlready
  ) where

import Control.Monad (unless, void, when)
import Control.Concurrent.MVar
import Control.Concurrent.ReadWriteVar (RWVar)
import Database.HDBC (SqlError (..), throwSqlError)
import Database.HDBC.ODBC.Api.Errors
import Database.HDBC.ODBC.Api.Imports
import Database.HDBC.ODBC.Api.Types
import Database.HDBC.ODBC.Log
import Foreign.Marshal hiding (void)
import Foreign.Ptr
import Foreign.Storable
import System.Mem.Weak (addFinalizer)

import qualified Control.Concurrent.ReadWriteVar as RWV
import qualified Data.Foldable as F

data EnvWrapper = EnvWrapper
  { envHandle :: MVar (Maybe SQLHENV) -- ^ If there is Nothing here, environment is already freed
  }

sqlAllocEnv :: IO EnvWrapper
sqlAllocEnv = do
    hEnv <- alloca $ \(penvptr :: Ptr SQLHENV) -> do
      retVal <- c_sqlAllocHandle sQL_HANDLE_ENV nullPtr (castPtr penvptr)
      unless (sqlSucceeded retVal) $ throwSqlError SqlError
        { seState = ""
        , seNativeError = -1
        , seErrorMsg = "sqlAllocEnv/SqlAllocHandle: Failed to allocate ODBC Environment handle"
        }
      peek penvptr

    handleVar <- newMVar (Just hEnv)
    let wrapper = EnvWrapper handleVar

    addFinalizer wrapper $ freeEnvIfNotAlready wrapper
    return wrapper

freeEnvIfNotAlready :: EnvWrapper -> IO ()
freeEnvIfNotAlready w = modifyMVar_ (envHandle w) $ \maybeEnv -> do
  F.forM_ maybeEnv $ \hEnv -> do
    hdbcTrace $ "Freeing environment with handle " ++ show hEnv
    void $ c_sqlFreeHandle sQL_HANDLE_ENV (castPtr hEnv)
  return Nothing

withMaybeEnv :: EnvWrapper -> (Maybe SQLHENV -> IO a) -> IO a
withMaybeEnv = withMVar . envHandle

withEnvOrDie :: EnvWrapper -> (SQLHENV -> IO a) -> IO a
withEnvOrDie ew act = withMaybeEnv ew $ \maybeHandle ->
  case maybeHandle of
    Just h -> act h
    Nothing -> do
      hdbcTrace "Requested an ENV handle from disposed wrapper. Throwing."
      throwSqlError SqlError
        { seState = ""
        , seNativeError = -1
        , seErrorMsg = "Tried to use a disposed ODBC Environment handle"
        }

data DbcWrapper = DbcWrapper
  { connHandle   :: RWVar (Maybe SQLHDBC) -- ^ If there is Nothing here, connection is already freed
  , connEnv      :: EnvWrapper
  , connOldStmts :: MVar [SQLHSTMT] -- ^ Statements that are no longer used and need to be freed
  }

-- This one implicitly allocates and initializes an environment.
sqlAllocDbc :: EnvWrapper -> IO DbcWrapper
sqlAllocDbc env = do
  hDbc <- withEnvOrDie env $ \hEnv -> alloca $ \(pdbcptr :: Ptr SQLHDBC) -> do
    retVal <- c_sqlAllocHandle sQL_HANDLE_DBC (castPtr hEnv) (castPtr pdbcptr)
    checkError "sqlAllocConn/SQLAllocHandle" (EnvHandle hEnv) retVal
    peek pdbcptr

  handleVar <- RWV.new $ Just hDbc
  oldStmtsVar <- newMVar []
  let wrapper = DbcWrapper handleVar env oldStmtsVar

  addFinalizer wrapper $ freeDbcIfNotAlready False wrapper
  return wrapper

freeOldStmts :: DbcWrapper -> IO ()
freeOldStmts dbc = RWV.with (connHandle dbc) $ \maybeConn ->
  F.forM_ maybeConn $ \_ ->
    modifyMVar_ (connOldStmts dbc) $ \stmts -> do
      F.forM_ stmts $ \hStmt -> do
        hdbcTrace $ "Freeing statement with handle " ++ show hStmt
        -- SQL Server might deadlock if closing handle to a statement in process of fetching
        -- network data so we have to cancel it explicitly. We also need to protect ourselves
        -- from trying to cancel or free a statement, which has its connection already finalized.
        void $ c_sqlCancel hStmt
        void $ c_sqlCloseCursor hStmt
        void $ c_sqlFreeHandle sQL_HANDLE_STMT (castPtr hStmt)
      return []

-- | Tries to perform disconnect and free resources used by connection. If SQLDisconnect call
-- fails, an exception gets thrown and connection resources aren't freed.
tryDisconnectAndFree :: DbcWrapper -> IO ()
tryDisconnectAndFree = freeDbcIfNotAlready True

freeDbcIfNotAlready :: Bool -> DbcWrapper -> IO ()
freeDbcIfNotAlready checkDisconnect dbc = do
  freeOldStmts dbc
  RWV.modify_ (connHandle dbc) $ \maybeHandle -> do
    F.forM_ maybeHandle $ \hDbc -> do
      hdbcTrace $ "Freeing connection with handle " ++ show hDbc
      disconnectResult <- c_sqlDisconnect hDbc
      when checkDisconnect $ checkError "freeDbcIfNotAlready/SQLDisconnect" (DbcHandle hDbc) disconnectResult
      void $ c_sqlFreeHandle sQL_HANDLE_DBC (castPtr hDbc)
    return Nothing
  freeEnvIfNotAlready $ connEnv dbc

withMaybeDbc :: DbcWrapper -> (Maybe SQLHDBC -> IO a) -> IO a
withMaybeDbc = RWV.with . connHandle

withDbcOrDie :: DbcWrapper -> (SQLHDBC -> IO a) -> IO a
withDbcOrDie dw act = withMaybeDbc dw $ \maybeHandle ->
  case maybeHandle of
    Just h -> act h
    Nothing -> do
      hdbcTrace "Requested a DBC handle from a disposed wrapper. Throwing"
      throwSqlError SqlError
        { seState = ""
        , seNativeError = -1
        , seErrorMsg = "Tried to use a disposed ODBC Connection handle"
        }

data StmtWrapper = StmtWrapper
  { stmtHandle :: MVar (Maybe SQLHSTMT) -- ^ If there is Nothing here, statement is already finished
  , stmtConn   :: DbcWrapper
  }

sqlAllocStmt :: DbcWrapper -> IO StmtWrapper
sqlAllocStmt dbc = do
  freeOldStmts dbc
  hStmt <- withDbcOrDie dbc $ \hDbc -> alloca $ \(psthptr :: Ptr SQLHSTMT) -> do
    retVal <- c_sqlAllocHandle sQL_HANDLE_STMT (castPtr hDbc) (castPtr psthptr)
    checkError "sqlAllocStmt/SQLAllocHandle" (DbcHandle hDbc) retVal
    peek psthptr

  handleVar <- newMVar $ Just hStmt
  let wrapper = StmtWrapper handleVar dbc

  addFinalizer wrapper $ freeStmtIfNotAlready wrapper
  return wrapper

freeStmtIfNotAlready :: StmtWrapper -> IO ()
freeStmtIfNotAlready stmt = modifyMVar_ (stmtHandle stmt) $ \maybeHandle -> do
  F.forM_ maybeHandle $ \hStmt ->
    modifyMVar_ (connOldStmts $ stmtConn stmt) $ return . (hStmt :)
  return Nothing

withMaybeStmt :: StmtWrapper -> (Maybe SQLHSTMT -> IO a) -> IO a
withMaybeStmt = withMVar . stmtHandle

withStmtOrDie :: StmtWrapper -> (SQLHSTMT -> IO a) -> IO a
withStmtOrDie sw act = withMaybeStmt sw $ \maybeHandle ->
  case maybeHandle of
    Just h -> act h
    Nothing -> do
      hdbcTrace $ "Requested a STMT handle from a disposed wrapper. Throwing."
      throwSqlError SqlError
        { seState = ""
        , seNativeError = -1
        , seErrorMsg = "Tried to use a disposed ODBC Statement handle"
        }