Added Raw Filesystem backend which does no encoding of its values, and therefore does not precisely conform to the laws a Tansu backend should have
Getty Ritter
9 years ago
| 10 | 10 | import Data.IORef |
| 11 | 11 | import Data.Map.Strict (Map) |
| 12 | 12 | import qualified Data.Map.Strict as M |
| 13 | import Data.Serialize (Serialize, encode) | |
| 14 | 13 | import Database.Tansu (Tansu) |
| 15 | 14 | import Database.Tansu.Internal |
| 16 | 15 | |
| 17 |
type Table = M |
|
| 16 | type Table = Map ByteString ByteString | |
| 18 | 17 | |
| 19 | 18 | ephemeralRunTransaction :: MVar () -> IO a -> IO a |
| 20 | 19 | ephemeralRunTransaction lock comp = withMVar lock $ \ () -> comp |
| 39 | 38 | newtype EphemeralDb = EDB { fromEDB :: Table } |
| 40 | 39 | |
| 41 | 40 | -- | Create an in-memory table to use for testing. |
| 42 | createEphemeralDb :: (Serialize k, Serialize v) => [(k, v)] -> EphemeralDb | |
| 43 | createEphemeralDb ls = EDB (M.fromList [ (encode k, encode v) | (k, v) <- ls ]) | |
| 41 | createEphemeralDb :: [(ByteString, ByteString)] -> EphemeralDb | |
| 42 | createEphemeralDb = EDB . M.fromList | |
| 44 | 43 | |
| 45 | 44 | -- | Run a 'Tansu' operation with an empty in-memory table. |
| 46 |
withNewEphemeralDb :: (TansuDb |
|
| 45 | withNewEphemeralDb :: (TansuDb -> IO a) -> IO a | |
| 47 | 46 | withNewEphemeralDb = withEphemeralDb $ EDB M.empty |
| 48 | 47 | |
| 49 | 48 | -- | Run a 'Tansu' operation with an existing in-memory table. |
| 50 |
withEphemeralDb :: EphemeralDb -> (TansuDb |
|
| 49 | withEphemeralDb :: EphemeralDb -> (TansuDb -> IO a) -> IO a | |
| 51 | 50 | withEphemeralDb init comp = do |
| 52 | 51 | lock <- newMVar () |
| 53 | 52 | table <- newIORef (fromEDB init) |
| 1 | {-# LANGUAGE ViewPatterns #-} | |
| 2 | ||
| 3 | module Database.Tansu.Backend.Filesystem.Raw (withRawFilesystemDb) where | |
| 4 | ||
| 5 | import Control.Exception | |
| 6 | import Data.ByteString (ByteString) | |
| 7 | import qualified Data.ByteString.Char8 as BS | |
| 8 | import Database.Tansu.Internal | |
| 9 | import System.Directory ( createDirectoryIfMissing | |
| 10 | , doesFileExist | |
| 11 | , removeFile | |
| 12 | ) | |
| 13 | import System.FileLock ( SharedExclusive(Exclusive) | |
| 14 | , withFileLock | |
| 15 | ) | |
| 16 | import System.FilePath ((</>), isValid) | |
| 17 | ||
| 18 | catchIO :: IO a -> IO (Either TansuError a) | |
| 19 | catchIO mote = fmap return mote `catch` go | |
| 20 | where go :: IOException -> IO (Either TansuError a) | |
| 21 | go = return . Left . OtherError . show | |
| 22 | ||
| 23 | filePathSet :: FilePath -> ByteString -> ByteString -> IO (Either TansuError ()) | |
| 24 | filePathSet path bsKey val = do | |
| 25 | let key = BS.unpack bsKey | |
| 26 | keyPath = path </> key | |
| 27 | if not (isValid key) | |
| 28 | then return (Left (OtherError ("Invalid file name: " ++ key))) | |
| 29 | else do | |
| 30 | let keyPath = path </> key | |
| 31 | catchIO $ BS.writeFile keyPath val | |
| 32 | ||
| 33 | filePathGet :: FilePath -> ByteString -> IO (Either TansuError ByteString) | |
| 34 | filePathGet path bsKey = do | |
| 35 | let key = BS.unpack bsKey | |
| 36 | keyPath = path </> key | |
| 37 | if not (isValid key) | |
| 38 | then return (Left (OtherError ("Invalid file name: " ++ key))) | |
| 39 | else do | |
| 40 | exists <- doesFileExist keyPath | |
| 41 | if exists | |
| 42 | then Right `fmap` BS.readFile keyPath | |
| 43 | else return (Left (KeyNotFound bsKey)) | |
| 44 | ||
| 45 | filePathDel :: FilePath -> ByteString -> IO (Either TansuError ()) | |
| 46 | filePathDel path bsKey = do | |
| 47 | let key = BS.unpack bsKey | |
| 48 | keyPath = path </> key | |
| 49 | if not (isValid key) | |
| 50 | then return (Left (OtherError ("Invalid file name: " ++ key))) | |
| 51 | else catchIO $ removeFile keyPath | |
| 52 | ||
| 53 | filePathLock :: FilePath -> IO a -> IO a | |
| 54 | filePathLock path comp = do | |
| 55 | withFileLock (path </> ".lock") Exclusive (const comp) | |
| 56 | ||
| 57 | -- | Use a local directory as a key-value store. Each key-value | |
| 58 | -- pair is represented as a file whose filename is the | |
| 59 | -- Base64-encoded serialized key, and whose content is the | |
| 60 | -- raw serialized value. Locking is done using the | |
| 61 | -- @.lock@ file in the specified directory, but note that | |
| 62 | -- file locking is not a guaranteed way of ensuring exclusion, | |
| 63 | -- and that the files themselves are not locked in any way. | |
| 64 | withRawFilesystemDb :: FilePath -> (TansuDb -> IO a) -> IO a | |
| 65 | withRawFilesystemDb path comp = do | |
| 66 | createDirectoryIfMissing True path | |
| 67 | comp $ TansuDb { dbSet = filePathSet path | |
| 68 | , dbGet = filePathGet path | |
| 69 | , dbDel = filePathDel path | |
| 70 | , dbRunTransaction = filePathLock path | |
| 71 | } |
| 46 | 46 | -- @.lock@ file in the specified directory, but note that |
| 47 | 47 | -- file locking is not a guaranteed way of ensuring exclusion, |
| 48 | 48 | -- and that the files themselves are not locked in any way. |
| 49 |
withFilesystemDb :: FilePath -> (TansuDb |
|
| 49 | withFilesystemDb :: FilePath -> (TansuDb -> IO a) -> IO a | |
| 50 | 50 | withFilesystemDb path comp = do |
| 51 | 51 | createDirectoryIfMissing True path |
| 52 | 52 | comp $ TansuDb { dbSet = filePathSet path |
| 23 | 23 | -- should treat this as an abstract type, but the full definition |
| 24 | 24 | -- is exposed by the "Database.Tansu.Internal" module so that |
| 25 | 25 | -- other libraries can implement new storage backends. |
| 26 | -- | |
| 27 | -- A given instantiation of the database will have | |
| 28 |
data TansuDb |
|
| 26 | data TansuDb = TansuDb | |
| 29 | 27 | { dbSet :: ByteString -> ByteString -> IO (Either TansuError ()) |
| 30 | 28 | , dbGet :: ByteString -> IO (Either TansuError ByteString) |
| 31 | 29 | , dbDel :: ByteString -> IO (Either TansuError ()) |
| 16 | 16 | |
| 17 | 17 | import Data.ByteString (ByteString) |
| 18 | 18 | import Data.Serialize (Serialize, encode, decode) |
| 19 | import MonadLib ( ReaderT | |
| 20 | , ExceptionT | |
| 21 | , ask | |
| 22 | , raise | |
| 23 | , inBase | |
| 24 | , runExceptionT | |
| 25 | , runReaderT | |
| 26 | ) | |
| 19 | import MonadLib (raise, try) | |
| 20 | import Database.Tansu.Monad | |
| 27 | 21 | import Database.Tansu.Internal |
| 28 | 22 | |
| 29 | type TansuM k v a = ReaderT (TansuDb k v) (ExceptionT TansuError IO) a | |
| 23 | newtype Tansu a = Tansu { runTansu :: TansuM a } | |
| 30 | 24 | |
| 31 | -- | The 'Tansu' type is a monad which represents some sequence | |
| 32 | -- of 'get' and 'set' operations. Ideally, backends should | |
| 33 | -- make some guarantee about the atomicity of a given | |
| 34 | -- 'Tansu' computation, but you should consult the documentation | |
| 35 | -- about a given backend to make sure that holds. | |
| 36 | newtype Tansu k v a = Tansu { runTansu :: TansuM k v a } | |
| 37 | ||
| 38 | instance Functor (Tansu k v) where | |
| 25 | instance Functor Tansu where | |
| 39 | 26 | fmap f (Tansu t) = Tansu (fmap f t) |
| 40 | 27 | |
| 41 |
instance Applicative |
|
| 28 | instance Applicative Tansu where | |
| 42 | 29 | pure = Tansu . pure |
| 43 | 30 | Tansu f <*> Tansu x = Tansu (f <*> x) |
| 44 | 31 | |
| 45 |
instance Monad |
|
| 32 | instance Monad Tansu where | |
| 46 | 33 | Tansu x >>= f = Tansu (x >>= runTansu . f) |
| 47 | 34 | |
| 48 | propagate :: IO (Either TansuError a) -> Tansu k v a | |
| 49 | propagate mote = do | |
| 50 | rs <- Tansu $ inBase $ mote | |
| 51 | case rs of | |
| 52 | Left err -> Tansu $ raise err | |
| 53 | Right x -> return x | |
| 54 | ||
| 55 | 35 | -- | Sets the value for a key to a value. |
| 56 | set :: (Serialize k, Serialize v) => k -> v -> Tansu k v () | |
| 57 | set key val = do | |
| 58 | db <- Tansu ask | |
| 59 | propagate $ dbSet db (encode key) (encode val) | |
| 36 | set :: ByteString -> ByteString -> Tansu () | |
| 37 | set key val = Tansu $ setInternal key val | |
| 60 | 38 | |
| 61 | 39 | -- | Sets the value for a key to a value. A convenience operator |
| 62 | 40 | -- that is identical to 'set'. |
| 63 |
(=:) :: |
|
| 41 | (=:) :: ByteString -> ByteString -> Tansu () | |
| 64 | 42 | (=:) = set |
| 65 | 43 | |
| 66 | 44 | -- | Gets a value for a given key. The resulting 'Tansu' computation |
| 67 | 45 | -- will fail if the key is not present in the storage backend. |
| 68 | get :: (Serialize k, Serialize v) => k -> Tansu k v v | |
| 69 | get key = do | |
| 70 | result <- getMb key | |
| 71 | case result of | |
| 72 | Just val -> return val | |
| 73 | Nothing -> Tansu (raise (KeyNotFound (encode key))) | |
| 46 | get :: ByteString -> Tansu ByteString | |
| 47 | get key = Tansu $ getInternal key | |
| 74 | 48 | |
| 75 | 49 | -- | Gets a value for a given key. If the key is not present, this |
| 76 | 50 | -- computation will return 'Nothing' instead. Other errors, such |
| 77 | 51 | -- as problems decoding the serialized value or difficulties |
| 78 | 52 | -- communicating with the storage backend, will still cause the |
| 79 | 53 | -- 'Tansu' computation to fail. |
| 80 |
getMb :: |
|
| 54 | getMb :: ByteString -> Tansu (Maybe ByteString) | |
| 81 | 55 | getMb key = do |
| 82 | db <- Tansu ask | |
| 83 | result <- Tansu $ inBase $ dbGet db (encode key) | |
| 84 |
|
|
| 56 | rs <- Tansu $ try $ runTansu $ get key | |
| 57 | case rs of | |
| 85 | 58 | Left (KeyNotFound _) -> return Nothing |
| 86 | 59 | Left err -> Tansu (raise err) |
| 87 | Right bs -> case decode bs of | |
| 88 | Right val' -> return (Just val') | |
| 89 |
|
|
| 60 | Right val -> return (Just val) | |
| 90 | 61 | |
| 91 | del :: (Serialize k) => k -> Tansu k v () | |
| 92 | del key = do | |
| 93 | db <- Tansu ask | |
| 94 | propagate $ dbDel db (encode key) | |
| 62 | -- | Delete a key. | |
| 63 | del :: ByteString -> Tansu () | |
| 64 | del key = Tansu $ delInternal key | |
| 95 | 65 | |
| 96 | 66 | -- | Given a storage backend and a 'Tansu' computation, execute the |
| 97 | 67 | -- sequence of 'get' and 'set' commands and produce either the value |
| 98 | 68 | -- or the error encountered while running the computation. |
| 99 | run :: TansuDb k v -> Tansu k v a -> IO (Either TansuError a) | |
| 100 | run db (Tansu comp) = | |
| 101 |
|
|
| 69 | run :: TansuDb -> Tansu a -> IO (Either TansuError a) | |
| 70 | run db (Tansu mote) = runInternal db mote |
| 5 | 5 | in part or in total at any time. |
| 6 | 6 | |
| 7 | 7 | The Tansu library is a minimal API for storing and recalling data in |
| 8 | key-value storage backends. The Tansu library does not intend to be useful | |
| 9 | for working with pre-existing data, as it makes assumptions about the | |
| 10 | formatting of keys and values. | |
| 8 | key-value storage backends. It is designed for new applications that | |
| 9 | have flexibility in terms of how they store information, as it may | |
| 10 | not provide the appropriate tools for working with data in existing | |
| 11 | key-value store backends. | |
| 11 | 12 | |
| 12 | 13 | ## Example |
| 13 | 14 | |
| 19 | 20 | import Data.Serialize (Serialize) |
| 20 | 21 | import GHC.Generics (Generic) |
| 21 | 22 | |
| 22 |
import Database.Tansu |
|
| 23 | import Database.Tansu.Serialize ((=:), get, runS, serialized) | |
| 23 | 24 | import Database.Tansu.Backend.Filesystem (withFilesystemDb) |
| 24 | 25 | |
| 25 | 26 | data Person = Person { name :: String, age :: Int } |
| 26 | 27 | deriving (Eq, Show, Generic, Serialize) |
| 27 | 28 | |
| 28 | 29 | main :: IO () |
| 29 | main = withFilesystemDb "sample.db" $ \ db -> do | |
| 30 | run db $ do | |
| 30 | main = withFilesystemDb "sample.db" $ serialized $ \ db -> do | |
| 31 | runS db $ do | |
| 31 | 32 | "alex" =: Person "Alex" 33 |
| 32 | 33 | "blake" =: Person "Blake" 22 |
| 33 | 34 | |
| 37 | 38 | |
| 38 | 39 | ## Use |
| 39 | 40 | |
| 40 | The Tansu API is very small and simple. All keys and values must implement | |
| 41 | the `Serialize` typeclass from the | |
| 42 | [`cereal`](https://hackage.haskell.org/package/cereal) | |
| 43 | library. No type information is saved in the key-value store, so care must | |
| 44 | be taken to ensure that the correct types are used in creating a `Tansu` | |
| 45 | computation. | |
| 41 | The `tansu` library exposes two near-identical APIs: the API | |
| 42 | in `Database.Tansu` allows you to store and write strict `ByteString` | |
| 43 | values, while the API in `Database.Tansu.Serialize` uses the typeclasses | |
| 44 | associated with the `cereal` library to allow a wider range of types to | |
| 45 | function as keys and values. The two libraries are otherwise very | |
| 46 | similar. | |
| 46 | 47 | |
| 47 | 48 | A value of type `TansuDb` represents a given key-value mapping. The only |
| 48 | 49 | way to interact with a `TansuDb` is by running a `Tansu` command, which |
| 53 | 54 | |
| 54 | 55 | ~~~.haskell |
| 55 | 56 | -- set a key to a value |
| 56 |
set :: |
|
| 57 | set :: ByteString -> ByteString -> Tansu () | |
| 57 | 58 | |
| 58 | 59 | -- infix alias for set |
| 59 |
(=:) :: |
|
| 60 | (=:) :: ByteString -> ByteString -> Tansu () | |
| 60 | 61 | |
| 61 | 62 | -- get a value, failing if it does not exist |
| 62 |
get :: |
|
| 63 | get :: ByteString -> Tansu ByteString | |
| 63 | 64 | |
| 64 | 65 | -- get a value, returning Nothing if it does not exist |
| 65 |
getMb :: |
|
| 66 | getMb :: ByteString -> Tansu (Maybe ByteString) | |
| 66 | 67 | |
| 67 | 68 | -- remove a key and its associated value |
| 68 |
del :: |
|
| 69 | del :: ByteString -> Tansu () | |
| 69 | 70 | |
| 70 | 71 | -- run a Tansu computation |
| 71 |
run :: TansuDb |
|
| 72 | run :: TansuDb -> Tansu a -> IO (Either TansuError a) | |
| 72 | 73 | ~~~ |
| 73 | 74 | |
| 74 | 75 | A value of type `TansuDb` should be supplied by a _backend_, which can |
| 91 | 92 | and the initial plan for the _tansu_ library was for it to be a convenient |
| 92 | 93 | API wrapper over the [Kyoto Cabinet](http://fallabs.com/kyotocabinet/) |
| 93 | 94 | library, but it has since become a generic wrapper over various |
| 94 |
key-value mapping backends. |
|
| 95 | key-value mapping backends. | |
| 1 | {-# LANGUAGE OverloadedStrings #-} | |
| 2 | ||
| 1 | 3 | module Main where |
| 2 | 4 | |
| 3 | 5 | import Control.Monad (void) |
| 4 | 6 | import Database.Tansu |
| 7 | import Database.Tansu.Backend.Ephemeral | |
| 5 | 8 | import Database.Tansu.Backend.Filesystem |
| 6 |
import Database.Tansu.Backend. |
|
| 9 | import Database.Tansu.Backend.Filesystem.Raw | |
| 7 | 10 | |
| 8 | 11 | main :: IO () |
| 9 | 12 | main = do |
| 10 | 13 | putStrLn "Testing filesystem db" |
| 11 |
withFilesystemDb "sample |
|
| 14 | withFilesystemDb "sample-fs.db" sample | |
| 15 | ||
| 16 | putStrLn "Testing raw filesystem db" | |
| 17 | withRawFilesystemDb "sample-raw.db" sample | |
| 12 | 18 | |
| 13 | 19 | putStrLn "Testing ephemeral db" |
| 14 | 20 | withNewEphemeralDb sample |
| 15 | 21 | |
| 16 |
sample :: TansuDb |
|
| 22 | sample :: TansuDb -> IO () | |
| 17 | 23 | sample db = do |
| 18 | 24 | putStrLn "Populating test database" |
| 19 | 25 | run db $ do |
| 25 | 31 | putStr "looking up key 'three': " |
| 26 | 32 | rs <- run db $ get "three" |
| 27 | 33 | case rs of |
| 28 |
Right val -> p |
|
| 34 | Right val -> print val | |
| 29 | 35 | Left _ -> putStrLn "...not in the database." |
| 30 | 36 | |
| 31 | 37 | putStr "looking up key 'five': " |
| 32 | 38 | rs <- run db $ get "five" |
| 33 | 39 | case rs of |
| 34 |
Right val -> p |
|
| 40 | Right val -> print val | |
| 35 | 41 | Left _ -> putStrLn "...not in the database." |
| 6 | 6 | license-file: LICENSE |
| 7 | 7 | author: Getty Ritter |
| 8 | 8 | maintainer: gettylefou@gmail.com |
| 9 | -- copyright: | |
| 9 | copyright: © 2016 Getty Ritter | |
| 10 | 10 | category: Database |
| 11 | 11 | build-type: Simple |
| 12 | 12 | extra-source-files: ChangeLog.md |
| 13 | 13 | cabal-version: >=1.10 |
| 14 | 14 | |
| 15 | source-repository head | |
| 16 | type: git | |
| 17 | location: git://github.com/aisamanra/tansu.git | |
| 18 | ||
| 19 | flag build-examples | |
| 20 | description: Build example applications | |
| 21 | default: False | |
| 22 | ||
| 15 | 23 | library |
| 16 | 24 | exposed-modules: Database.Tansu, |
| 17 | 25 | Database.Tansu.Internal, |
| 18 |
Database.Tansu. |
|
| 26 | Database.Tansu.Serialize, | |
| 27 | Database.Tansu.Backend.Ephemeral, | |
| 19 | 28 | Database.Tansu.Backend.Filesystem, |
| 20 |
Database.Tansu.Backend. |
|
| 29 | Database.Tansu.Backend.Filesystem.Raw | |
| 30 | other-modules: Database.Tansu.Monad | |
| 21 | 31 | build-depends: base >=4.8 && <4.9, |
| 22 | 32 | bytestring, |
| 23 | 33 | cereal, |
| 30 | 40 | filelock |
| 31 | 41 | default-language: Haskell2010 |
| 32 | 42 | |
| 33 |
executable sample |
|
| 43 | executable sample1 | |
| 44 | if !flag(build-examples) | |
| 45 | buildable: False | |
| 34 | 46 | main-is: Main.hs |
| 35 | 47 | build-depends: base, tansu |
| 36 |
hs-source-dirs: sample |
|
| 48 | hs-source-dirs: sample1 | |
| 37 | 49 | default-language: Haskell2010 |
| 50 | ||
| 51 | ||
| 52 | executable sample2 | |
| 53 | if !flag(build-examples) | |
| 54 | buildable: False | |
| 55 | main-is: Main.hs | |
| 56 | build-depends: base, tansu, cereal | |
| 57 | hs-source-dirs: sample2 | |
| 58 | default-language: Haskell2010 | |