{-| Implementation of an SQLite3-based event store. -}
module Data.CQRS.EventStore.Backend.Sqlite3Utils
       ( sourceQuery
       , execSql
       , withTransaction
       ) where

import           Control.Exception (catch, bracket, onException, SomeException)
import           Control.Monad.IO.Class (liftIO)
import           Data.Conduit (ResourceT, Source, bracketP, yield)
import           Data.Text (Text)
import qualified Database.SQLite3 as SQL
import           Database.SQLite3 (Database, Statement, SQLData(..), StepResult(..))

beginTransaction :: Database -> IO ()
beginTransaction database = execSql database "BEGIN TRANSACTION;" []

commitTransaction :: Database -> IO ()
commitTransaction database = execSql database "COMMIT TRANSACTION;" []

rollbackTransaction :: Database -> IO ()
rollbackTransaction database = execSql database "ROLLBACK TRANSACTION;" []

withSqlStatement :: Database -> Text -> [SQLData] -> (Statement -> IO a) -> IO a
withSqlStatement database sql parameters action =
  bracket (SQL.prepare database sql) SQL.finalize $ \statement -> do
    SQL.bind statement parameters
    action statement

-- | Execute an SQL statement for which no result is expected.
execSql :: Database -> Text -> [SQLData] -> IO ()
execSql database sql parameters =
  withSqlStatement database sql parameters $ \stmt -> do
    _ <- SQL.step stmt
    return ()

data State = Unbound | Bound
           deriving (Eq)

sourceQuery :: Database -> Text -> [SQLData] -> Source (ResourceT IO) [SQLData]
sourceQuery database sql parameters =
    bracketP (SQL.prepare database sql)
             (SQL.finalize)
             run
    where
      run stmt = do
        liftIO $ SQL.bind stmt parameters
        loop stmt
      loop stmt = do
        nextResult <- liftIO $ SQL.step stmt
        case nextResult of
          Done -> return ()
          Row -> do
            (liftIO $ SQL.columns stmt) >>= yield
            loop stmt

-- | Execute an IO action with an active transaction.
withTransaction :: Database -> IO a -> IO a
withTransaction database action = do
  beginTransaction database
  onException runAction tryRollback
  where
    runAction = do
      r <- action
      commitTransaction database
      return r

    tryRollback =
      -- Try rollback while discarding exception; we want to preserve
      -- original exception.
      catch (rollbackTransaction database) (\(_::SomeException) -> return ())