packages feed

SciFlow-0.7.0: src/Control/Workflow/DataStore.hs

{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
module Control.Workflow.DataStore
    ( DataStore(..)
    , Key
    , mkKey
    , JobStatus(..)
    , openStore
    , closeStore
    , withStore
    , markPending
    , markFailed
    , saveItem
    , fetchItem
    , delRecord
    , queryStatus
    ) where

import Control.Monad (unless)
import Control.Concurrent.MVar (withMVar, newMVar, MVar)
import Control.Monad.Catch (MonadMask, bracket)
import Control.Monad.Identity (Identity(..))
import Data.Binary (Binary, encode, decode)
import Data.Typeable (Typeable, typeOf)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Database.SQLite.Simple
import GHC.Generics (Generic)
import qualified Data.Text as T
import qualified Data.ByteString.Char8 as B
import qualified Crypto.Hash.SHA256 as C
import Data.ByteArray.Encoding (convertToBase, Base(..))

newtype DataStore = DataStore { _db_conn :: MVar Connection }

data Key = Key { _hash :: B.ByteString, _name :: T.Text }

instance Show Key where
    show (Key hash jn) = T.unpack jn <> "(" <> h <> ")"
      where
        h = B.unpack (B.take 4 hash) <> ".."

mkKey :: (Typeable i, Binary i) => i -> T.Text -> Key
mkKey input nm = Key hash nm
  where
    hash = convertToBase Base16 $ C.hashlazy $ encode (nm, show $ typeOf input, input)
{-# INLINE mkKey #-}

data JobStatus = Pending
               | Complete
               | Failed String
               deriving (Eq, Generic, Show)

instance Binary JobStatus

openStore :: MonadIO m => FilePath -> m DataStore
openStore root = liftIO $ do
    db <- open root
    itemExist <- hasTable db "item_db"
    metaExist <- hasTable db "meta_db"
    unless (itemExist && metaExist) $ do
        execute_ db "CREATE TABLE item_db(hash TEXT PRIMARY KEY, data BLOB)"
        execute_ db
            "CREATE TABLE meta_db(hash TEXT PRIMARY KEY, jobname TEXT, status BLOB)"
        execute_ db "CREATE INDEX jobname_index ON meta_db(jobname)"
    DataStore <$> newMVar db
{-# INLINE openStore #-}

closeStore :: MonadIO m => DataStore -> m ()
closeStore (DataStore db) = liftIO $ withMVar db close
{-# INLINE closeStore #-}

withStore :: (MonadIO m, MonadMask m)
          => FilePath -> (DataStore -> m a) -> m a
withStore root = bracket (openStore root) closeStore
{-# INLINE withStore #-}

markPending :: MonadIO m => DataStore -> Key -> m ()
markPending (DataStore store) (Key k n) = liftIO $ withMVar store $ \db ->
    execute db "REPLACE INTO meta_db VALUES (?, ?, ?)" (k, n, encode Pending)
{-# INLINE markPending #-}

markFailed :: MonadIO m => DataStore -> Key -> String -> m ()
markFailed (DataStore store) (Key k n) msg  = liftIO $ withMVar store $ \db ->
    execute db "REPLACE INTO meta_db VALUES (?, ?, ?)" (k, n, encode $ Failed msg)
{-# INLINE markFailed #-}

queryStatus :: MonadIO m => DataStore -> Key -> m (Maybe JobStatus)
queryStatus (DataStore store) (Key k _) = liftIO $ withMVar store $ \db ->
    query db "SELECT status FROM meta_db WHERE hash=?" [k] >>= \case
        [Only result] -> return $ Just $ decode result
        _ -> return Nothing
{-# INLINE queryStatus #-}

saveItem :: (MonadIO m, Binary a) => DataStore -> Key -> a -> m ()
saveItem (DataStore store) (Key k n) res = liftIO $ withMVar store $ \db -> do
    execute db "REPLACE INTO item_db VALUES (?, ?)" (k, encode res)
    execute db "REPLACE INTO meta_db VALUES (?, ?, ?)" (k, n, encode Complete)
{-# INLINE saveItem #-}

fetchItem :: (MonadIO m, Binary a) => DataStore -> Key -> m a
fetchItem (DataStore store) (Key k _) = liftIO $ withMVar store $ \db -> do
    query db "SELECT data FROM item_db WHERE hash=?" [k] >>= \case
        [Only result] -> return $ decode result
        _ -> error "Item not found"
{-# INLINE fetchItem #-}

delRecord :: MonadIO m => DataStore -> Key -> m ()
delRecord (DataStore store) (Key k _) = liftIO $ withMVar store $ \db -> do
    execute db "DELETE FROM meta_db WHERE hash= ?" [k]
    execute db "DELETE FROM item_db WHERE hash= ?" [k]
{-# INLINE delRecord #-}

-------------------------------------------------------------------------------
-- Low level functions
-------------------------------------------------------------------------------

hasTable :: Connection -> String -> IO Bool
hasTable db tablename = do
    r <- query db
        "SELECT name FROM sqlite_master WHERE type='table' AND name=?" [tablename]
    return $ not $ null (r :: [Only T.Text])
{-# INLINE hasTable #-}

{-
getKeys :: Connection -> IO [Key]
getKeys db = concat <$> query_ db
    (Query $ T.pack $ printf "SELECT pid FROM %s;" dbTableName)
-}