packages feed

lmdb-simple (empty) → 0.1.0.0

raw patch · 15 files changed

+1396/−0 lines, 15 filesdep +QuickCheckdep +basedep +binarysetup-changed

Dependencies added: QuickCheck, base, binary, bytestring, criterion, hspec, lmdb, lmdb-simple

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Robert Leslie (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Robert Leslie nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,7 @@++Simple Haskell API for LMDB+===========================++This is a simple API for the [Lightning Memory-mapped Database][LMDB].++  [LMDB]: https://symas.com/lightning-memory-mapped-database/
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lmdb-simple.cabal view
@@ -0,0 +1,86 @@++name:                lmdb-simple+version:             0.1.0.0++synopsis:            Simple API for LMDB++description:         This package provides a simple API for using the+                     Lightning Memory-mapped Database (LMDB).++homepage:            https://github.com/verement/lmdb-simple#readme+bug-reports:         https://github.com/verement/lmdb-simple/issues++license:             BSD3+license-file:        LICENSE++author:              Rob Leslie+maintainer:          rob@mars.org+copyright:           © 2017 Robert Leslie++stability:           experimental+category:            Database++build-type:          Simple+cabal-version:       >=1.10++extra-source-files:  README.md+                     stack.yaml+                     test/env/.keep++source-repository head+  type:              git+  location:          https://github.com/verement/lmdb-simple.git++library+  hs-source-dirs:      src++  exposed-modules:     Database.LMDB.Simple+                       Database.LMDB.Simple.Extra+                       Database.LMDB.Simple.View+  other-modules:       Database.LMDB.Simple.Internal++  build-depends:       base >= 4.7 && < 5+                     , binary >= 0.8+                     , bytestring+                     , lmdb >= 0.2+  ghc-options:         -Wall -Wno-name-shadowing -Wno-unused-do-bind+  default-language:    Haskell2010+  default-extensions:  Trustworthy+  other-extensions:    ConstraintKinds+                       KindSignatures+                       TypeFamilies++test-suite sample+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             sample.hs+  build-depends:       base+                     , lmdb-simple+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++test-suite hspec+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             hspec.hs+  other-modules:       Database.LMDB.SimpleSpec+                       Harness+  build-depends:       base+                     , hspec+                     , lmdb-simple+                     , QuickCheck+  ghc-options:         -Wall -Wno-unused-do-bind+                       -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++benchmark criterion+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             criterion.hs+  other-modules:       Harness+  build-depends:       base+                     , criterion+                     , lmdb-simple+  ghc-options:         -Wall -Wno-name-shadowing+                       -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010
+ src/Database/LMDB/Simple.hs view
@@ -0,0 +1,331 @@++{-# LANGUAGE ConstraintKinds #-}++{-|+Module      : Database.LMDB.Simple+Description : Simple Haskell API for LMDB+Copyright   : © 2017 Robert Leslie+License     : BSD3+Maintainer  : rob@mars.org+Stability   : experimental++This module provides a simple Haskell API for the+<https://symas.com/lightning-memory-mapped-database/ Lightning Memory-mapped Database>+(LMDB).++Example usage:++@+module Main where++import Database.LMDB.Simple+import Control.Monad (forM_)++main = do+  env <- openEnvironment "myenv" defaultLimits+  db <- readOnlyTransaction env $ getDatabase Nothing :: IO (Database String Int)++  transaction env $+    forM_ [("one",1),("two",2),("three",3)] $ \\(k,v) -> put db k (Just v)++  print =<< readOnlyTransaction env (get db "two")   -- Just 2+  print =<< readOnlyTransaction env (get db "nine")  -- Nothing+@++Additional functions for querying and modifying LMDB databases are provided in+"Database.LMDB.Simple.Extra". For an option to access LMDB databases from pure+code, see "Database.LMDB.Simple.View".+-}++module Database.LMDB.Simple+  ( -- * Environments+    Environment+  , Limits (..)+  , defaultLimits+  , openEnvironment+  , openReadWriteEnvironment+  , openReadOnlyEnvironment+  , clearStaleReaders++    -- * Transactions+  , Transaction+  , transaction+  , readWriteTransaction+  , readOnlyTransaction+  , nestTransaction+  , abort+  , AbortedTransaction++    -- * Databases+  , Database+  , getDatabase+  , get+  , put+  , clear++    -- * Access modes+  , ReadWrite+  , ReadOnly+  , Mode+  , SubMode+  ) where++import Prelude hiding+  ( lookup+  )++import Control.Concurrent+  ( runInBoundThread+  )++import Control.Exception+  ( Exception+  , throwIO+  , try+  , tryJust+  , bracketOnError+  )++import Control.Monad+  ( guard+  , void+  )++import Data.Binary+  ( Binary+  )++import Database.LMDB.Raw+  ( LMDB_Error (LMDB_Error, e_code)+  , MDB_EnvFlag (MDB_NOSUBDIR, MDB_RDONLY)+  , MDB_DbFlag (MDB_CREATE)+  , mdb_env_create+  , mdb_env_open+  , mdb_env_set_mapsize+  , mdb_env_set_maxdbs+  , mdb_env_set_maxreaders+  , mdb_dbi_open'+  , mdb_txn_begin+  , mdb_txn_commit+  , mdb_txn_abort+  , mdb_txn_env+  , mdb_clear'+  , mdb_reader_check+  )++import Database.LMDB.Simple.Internal+  ( ReadWrite+  , ReadOnly+  , Mode+  , SubMode+  , Environment (Env)+  , Transaction (Txn)+  , Database (Db)+  , isReadOnlyEnvironment+  , isReadOnlyTransaction+  , isReadWriteTransaction+  )++import qualified Database.LMDB.Simple.Internal as Internal++import Foreign.C+  ( Errno (Errno)+  , eNOTDIR+  )++-- | LMDB environments have various limits on the size and number of databases+-- and concurrent readers.+data Limits = Limits+  { mapSize      :: Int  -- ^ memory map size, in bytes (also the maximum size+                         -- of all databases)+  , maxDatabases :: Int  -- ^ maximum number of named databases+  , maxReaders   :: Int  -- ^ maximum number of concurrent 'ReadOnly'+                         -- transactions (also the number of slots in the lock+                         -- table)+  }++-- | The default limits are 1 MiB map size, 0 named databases, and 126+-- concurrent readers. These can be adjusted freely, and in particular the+-- 'mapSize' may be set very large (limited only by available address+-- space). However, LMDB is not optimized for a large number of named+-- databases so 'maxDatabases' should be kept to a minimum.+--+-- The default 'mapSize' is intentionally small, and should be changed to+-- something appropriate for your application. It ought to be a multiple of+-- the OS page size, and should be chosen as large as possible to accommodate+-- future growth of the database(s). Once set for an environment, this limit+-- cannot be reduced to a value smaller than the space already consumed by the+-- environment, however it can later be increased.+--+-- If you are going to use any named databases then you will need to change+-- 'maxDatabases' to the number of named databases you plan to use. However,+-- you do not need to change this field if you are only going to use the+-- single main (unnamed) database.+defaultLimits :: Limits+defaultLimits = Limits+  { mapSize      = 1024 * 1024  -- 1 MiB+  , maxDatabases = 0+  , maxReaders   = 126+  }++-- | Open an LMDB environment in either 'ReadWrite' or 'ReadOnly' mode. The+-- 'FilePath' argument may be either a directory or a regular file, but it+-- must already exist. If a regular file, an additional file with "-lock"+-- appended to the name is used for the reader lock table.+--+-- Note that an environment must have been opened in 'ReadWrite' mode at least+-- once before it can be opened in 'ReadOnly' mode.+--+-- An environment opened in 'ReadOnly' mode may still modify the reader lock+-- table (except when the filesystem is read-only, in which case no locks are+-- used).+openEnvironment :: Mode mode => FilePath -> Limits -> IO (Environment mode)+openEnvironment path limits = do+  env <- mdb_env_create++  mdb_env_set_mapsize    env (mapSize      limits)+  mdb_env_set_maxdbs     env (maxDatabases limits)+  mdb_env_set_maxreaders env (maxReaders   limits)++  let environ = Env env :: Mode mode => Environment mode+      flags   = [MDB_RDONLY | isReadOnlyEnvironment environ]++  r <- tryJust (guard . isNotDirectoryError) $ mdb_env_open env path flags+  case r of+    Left  _ -> mdb_env_open env path (MDB_NOSUBDIR : flags)+    Right _ -> return ()++  return environ++  where isNotDirectoryError :: LMDB_Error -> Bool+        isNotDirectoryError LMDB_Error { e_code = Left code }+          | Errno (fromIntegral code) == eNOTDIR = True+        isNotDirectoryError _                    = False++-- | Convenience function for opening an LMDB environment in 'ReadWrite'+-- mode; see 'openEnvironment'+openReadWriteEnvironment :: FilePath -> Limits -> IO (Environment ReadWrite)+openReadWriteEnvironment = openEnvironment++-- | Convenience function for opening an LMDB environment in 'ReadOnly'+-- mode; see 'openEnvironment'+openReadOnlyEnvironment :: FilePath -> Limits -> IO (Environment ReadOnly)+openReadOnlyEnvironment = openEnvironment++-- | Check for stale entries in the reader lock table, and return the number+-- of entries cleared.+clearStaleReaders :: Environment mode -> IO Int+clearStaleReaders (Env env) = mdb_reader_check env++-- | Perform a top-level transaction in either 'ReadWrite' or 'ReadOnly'+-- mode. A transaction may only be 'ReadWrite' if the environment is also+-- 'ReadWrite' (enforced by the 'SubMode' constraint).+--+-- Once completed, the transaction will be committed and the result+-- returned. An exception will cause the transaction to be implicitly aborted.+--+-- Note that there may be several concurrent 'ReadOnly' transactions (limited+-- only by the 'maxReaders' field of the 'Limits' argument given to+-- 'openEnvironment'), but there is at most one active 'ReadWrite'+-- transaction, which is forced to run in a bound thread, and is protected by+-- an internal mutex.+--+-- In general, long-lived transactions should be avoided. 'ReadOnly'+-- transactions prevent reuse of database pages freed by newer 'ReadWrite'+-- transactions, and thus the database can grow quickly. 'ReadWrite'+-- transactions prevent other 'ReadWrite' transactions, since writes are+-- serialized.+transaction :: (Mode tmode, SubMode emode tmode)+            => Environment emode -> Transaction tmode a -> IO a+transaction (Env env) tx@(Txn tf)+  | isReadOnlyTransaction tx = run True+  | otherwise                = runInBoundThread (run False)+  where run readOnly =+          bracketOnError (mdb_txn_begin env Nothing readOnly) mdb_txn_abort $+          \txn -> tf txn >>= \result -> mdb_txn_commit txn >> return result++-- | The exception type thrown when a (top-level) transaction is explicitly+-- aborted+data AbortedTransaction = AbortedTransaction++instance Show AbortedTransaction where+  showsPrec _ AbortedTransaction = showString "aborted transaction"++instance Exception AbortedTransaction++-- | Convenience function for performing a top-level 'ReadWrite' transaction;+-- see 'transaction'+readWriteTransaction :: Environment ReadWrite+                     -> Transaction ReadWrite a -> IO a+readWriteTransaction = transaction++-- | Convenience function for performing a top-level 'ReadOnly' transaction;+-- see 'transaction'+readOnlyTransaction :: Environment mode+                    -> Transaction ReadOnly a -> IO a+readOnlyTransaction = transaction++-- | Nest a transaction within the current 'ReadWrite' transaction.+-- Transactions may be nested to any level.+--+-- If the nested transaction is aborted, 'Nothing' is returned. Otherwise, the+-- nested transaction is committed and the result is returned in a 'Just'+-- value. (The overall effect of a nested transaction depends on whether the+-- parent transaction is ultimately committed.)+--+-- An exception will cause the nested transaction to be implicitly aborted.+nestTransaction :: Transaction ReadWrite a -> Transaction ReadWrite (Maybe a)+nestTransaction tx@(Txn tf) = Txn $ run (isReadOnlyTransaction tx)+  where run ro ptxn = let env = mdb_txn_env ptxn in maybeAborted $+          bracketOnError (mdb_txn_begin env (Just ptxn) ro) mdb_txn_abort $+          \ctxn -> tf ctxn >>= \result -> mdb_txn_commit ctxn >> return result++        maybeAborted :: IO a -> IO (Maybe a)+        maybeAborted io = either+          (\e -> let _ = e :: AbortedTransaction in Nothing) Just <$> try io++-- | Explicitly abort the current transaction, nullifying its effects on the+-- LMDB environment. No further actions will be performed within the current+-- transaction.+--+-- In a nested transaction, this causes the child transaction to return+-- 'Nothing' to its parent. In a top-level transaction, this throws an+-- 'AbortedTransaction' exception, which can be caught.+abort :: Transaction mode a+abort = Txn $ \_ -> throwIO AbortedTransaction++-- | Retrieve a database handle from the LMDB environment. The database may be+-- specified by name, or 'Nothing' can be used to specify the main (unnamed)+-- database for the environment. If a named database is specified, it must+-- already exist, or it will be created if the transaction is 'ReadWrite'.+--+-- There are a limited number of named databases you may use in an+-- environment, set by the 'maxDatabases' field of the 'Limits' argument given+-- to 'openEnvironment'. By default ('defaultLimits') this number is zero, so+-- you must specify another limit in order to use any named databases.+--+-- You should not use both named and unnamed databases in the same+-- environment, because the unnamed database is used internally to store+-- entries for each named database.+--+-- You can (and should) retain the database handle returned by this action for+-- use in future transactions.+getDatabase :: Mode mode => Maybe String -> Transaction mode (Database k v)+getDatabase name = tx+  where tx = Txn $ \txn -> Db (mdb_txn_env txn) <$> mdb_dbi_open' txn name flags+        flags = [MDB_CREATE | isReadWriteTransaction tx]++-- | Lookup a key in a database and return the corresponding value, or return+-- 'Nothing' if the key does not exist in the database.+get :: (Binary k, Binary v) => Database k v -> k -> Transaction mode (Maybe v)+get = Internal.get++-- | Insert the given key/value pair into a database, or delete the key from+-- the database if 'Nothing' is given for the value.+put :: (Binary k, Binary v)+    => Database k v -> k -> Maybe v -> Transaction ReadWrite ()+put db key = maybe (void $ Internal.delete db key) (Internal.put db key)++-- | Delete all key/value pairs from a database, leaving the database empty.+clear :: Database k v -> Transaction ReadWrite ()+clear (Db _ dbi) = Txn $ \txn -> mdb_clear' txn dbi
+ src/Database/LMDB/Simple/Extra.hs view
@@ -0,0 +1,296 @@++-- | This module exports many functions for querying and modifying LMDB+-- databases using common idioms (albeit in monadic form).++module Database.LMDB.Simple.Extra+  ( -- * Query+    null+  , size+  , member+  , notMember+  , lookup+  , findWithDefault++    -- * Modification++    -- ** Insertion+  , insert+  , insertWith+  , insertWithKey+  , insertLookupWithKey++    -- ** Delete/Update+  , delete+  , adjust+  , adjustWithKey+  , update+  , updateWithKey+  , updateLookupWithKey+  , alter++    -- * Folds+  , foldr+  , foldl+  , foldrWithKey+  , foldlWithKey+  , foldDatabaseWithKey++    -- * Conversion+  , elems+  , keys+  , toList+  ) where++import Prelude hiding+  ( foldl+  , foldr+  , lookup+  , null+  )++import Control.Monad+  ( void+  )++import Data.Binary+  ( Binary+  )++import Data.Maybe+  ( fromMaybe+  , isJust+  )++import Database.LMDB.Raw+  ( MDB_stat (ms_entries)+  , MDB_val+  , MDB_cursor_op (MDB_SET)+  , MDB_cursor'+  , MDB_WriteFlags+  , mdb_stat'+  , mdb_cursor_get'+  , mdb_cursor_put'+  , mdb_cursor_del'+  )++import Database.LMDB.Simple.Internal+  ( ReadWrite+  , Transaction (Txn)+  , Database (Db)+  , forEachForward+  , forEachReverse+  , marshalOut+  , peekVal+  , withCursor+  , defaultWriteFlags+  , overwriteFlags+  )+import qualified Database.LMDB.Simple.Internal as Internal++import Foreign+  ( alloca+  , nullPtr+  , with+  )++-- | Lookup the value at a key in the database.+--+-- The function will return the corresponding value as @('Just' value)@, or+-- 'Nothing' if the key isn't in the database.+lookup :: (Binary k, Binary v) => k -> Database k v -> Transaction mode (Maybe v)+lookup = flip Internal.get++-- | The expression @('findWithDefault' def k db)@ returns the value at key+-- @k@ or returns default value @def@ when the key is not in the database.+findWithDefault :: (Binary k, Binary v)+                => v -> k -> Database k v -> Transaction mode v+findWithDefault def key db = fromMaybe def <$> lookup key db++-- | Is the database empty?+null :: Database k v -> Transaction mode Bool+null (Db _ dbi) = Txn $ \txn -> do+  stat <- mdb_stat' txn dbi+  return (ms_entries stat == 0)++-- | The number of entries in the database.+size :: Database k v -> Transaction mode Int+size (Db _ dbi) = Txn $ \txn -> do+  stat <- mdb_stat' txn dbi+  return (fromIntegral $ ms_entries stat)++-- | Is the key a member of the database? See also 'notMember'.+member :: Binary k => k -> Database k v -> Transaction mode Bool+member key db = isJust <$> Internal.get' db key++-- | Is the key not a member of the database? See also 'member'.+notMember :: Binary k => k -> Database k v -> Transaction mode Bool+notMember key db = not <$> member key db++-- | Insert a new key and value in the database. If the key is already present+-- in the database, the associated value is replaced with the supplied+-- value. 'insert' is equivalent to @'insertWith' 'const'@.+insert :: (Binary k, Binary v) => k -> v -> Database k v+       -> Transaction ReadWrite ()+insert key value db = Internal.put db key value++-- | Insert with a function, combining new value and old value. @'insertWith'+-- f key value db@ will insert the pair @(key, value)@ into @db@ if key does+-- not exist in the database. If the key does exist, the function will insert+-- the pair @(key, f new_value old_value)@.+insertWith :: (Binary k, Binary v) => (v -> v -> v) -> k -> v -> Database k v+           -> Transaction ReadWrite ()+insertWith f = insertWithKey (const f)++-- | Insert with a function, combining key, new value and old+-- value. @'insertWithKey' f key value db@ will insert the pair @(key, value)@+-- into @db@ if key does not exist in the database. If the key does exist, the+-- function will insert the pair @(key, f key new_value old_value)@. Note that+-- the key passed to @f@ is the same key passed to 'insertWithKey'.+insertWithKey :: (Binary k, Binary v) => (k -> v -> v -> v) -> k -> v+              -> Database k v -> Transaction ReadWrite ()+insertWithKey f key value = void . insertLookupWithKey f key value++-- | Combines insert operation with old value retrieval. The monadic action+-- @('insertLookupWithKey' f k x db)@ returns the same value as @('lookup' k+-- db)@ but has the same effect as @('insertWithKey' f k x db)@.+insertLookupWithKey :: (Binary k, Binary v) => (k -> v -> v -> v) -> k -> v+                    -> Database k v -> Transaction ReadWrite (Maybe v)+insertLookupWithKey f key value (Db _ dbi) = Txn $ \txn ->+  withCursor txn dbi $ \cursor -> marshalOut key $ \kval ->+  with kval $ \kptr -> alloca $ \vptr -> do+    found <- mdb_cursor_get' MDB_SET cursor kptr vptr+    if found+      then do oldValue <- peekVal vptr+              cursorPut cursor overwriteFlags kval (f key value oldValue)+              return (Just oldValue)+      else do cursorPut cursor defaultWriteFlags kval value+              return  Nothing++  where cursorPut :: Binary v => MDB_cursor' -> MDB_WriteFlags -> MDB_val -> v+                  -> IO Bool+        cursorPut cursor writeFlags kval value = marshalOut value $ \vval ->+          mdb_cursor_put' writeFlags cursor kval vval++-- | Return all elements of the database in the order of their keys.+elems :: Binary v => Database k v -> Transaction mode [v]+elems = foldr (:) []++-- | Return all keys of the database in the order they are stored on disk.+keys :: Binary k => Database k v -> Transaction mode [k]+keys (Db _ dbi) = Txn $ \txn ->+  alloca $ \kptr ->+  forEachForward txn dbi kptr nullPtr [] $ \rest ->+  (:) <$> peekVal kptr <*> rest++-- | Convert the database to a list of key/value pairs. Note that this will+-- make a copy of the entire database in memory.+toList :: (Binary k, Binary v) => Database k v -> Transaction mode [(k, v)]+toList = foldrWithKey (\k v -> ((k, v) :)) []++-- | Fold the values in the database using the given right-associative binary+-- operator.+foldr :: Binary v => (v -> b -> b) -> b -> Database k v -> Transaction mode b+foldr f z (Db _ dbi) = Txn $ \txn ->+  alloca $ \vptr ->+  forEachForward txn dbi nullPtr vptr z $ \rest ->+  f <$> peekVal vptr <*> rest++-- | Fold the keys and values in the database using the given+-- right-associative binary operator.+foldrWithKey :: (Binary k, Binary v)+             => (k -> v -> b -> b) -> b -> Database k v -> Transaction mode b+foldrWithKey f z (Db _ dbi) = Txn $ \txn ->+  alloca $ \kptr ->+  alloca $ \vptr ->+  forEachForward txn dbi kptr vptr z $ \rest ->+  f <$> peekVal kptr <*> peekVal vptr <*> rest++-- | Fold the values in the database using the given left-associative binary+-- operator.+foldl :: Binary v => (a -> v -> a) -> a -> Database k v -> Transaction mode a+foldl f z (Db _ dbi) = Txn $ \txn ->+  alloca $ \vptr ->+  forEachReverse txn dbi nullPtr vptr z $ \rest ->+  flip f <$> peekVal vptr <*> rest++-- | Fold the keys and values in the database using the given left-associative+-- binary operator.+foldlWithKey :: (Binary k, Binary v)+             => (a -> k -> v -> a) -> a -> Database k v -> Transaction mode a+foldlWithKey f z (Db _ dbi) = Txn $ \txn ->+  alloca $ \kptr ->+  alloca $ \vptr ->+  forEachReverse txn dbi kptr vptr z $ \rest ->+  (\k v a -> f a k v) <$> peekVal kptr <*> peekVal vptr <*> rest++-- | Fold the keys and values in the database using the given monoid.+foldDatabaseWithKey :: (Monoid m, Binary k, Binary v)+                    => (k -> v -> m) -> Database k v -> Transaction mode m+foldDatabaseWithKey f = foldrWithKey (\k v a -> f k v `mappend` a) mempty++-- | Delete a key and its value from the database. If the key was not present+-- in the database, this returns 'False'; otherwise it returns 'True'.+delete :: Binary k => k -> Database k v -> Transaction ReadWrite Bool+delete = flip Internal.delete++-- | Update a value at a specific key with the result of the provided+-- function. When the key is not a member of the database, this returns+-- 'False'; otherwise it returns 'True'.+adjust :: (Binary k, Binary v) => (v -> v) -> k+       -> Database k v -> Transaction ReadWrite Bool+adjust f = adjustWithKey (const f)++-- | Adjust a value at a specific key. When the key is not a member of the+-- database, this returns 'False'; otherwise it returns 'True'.+adjustWithKey :: (Binary k, Binary v) => (k -> v -> v) -> k+              -> Database k v -> Transaction ReadWrite Bool+adjustWithKey f = updateWithKey (\k v -> Just $ f k v)++-- | The monadic action @('update' f k db)@ updates the value @x@ at @k@ (if+-- it is in the database). If @(f x)@ is 'Nothing', the element is deleted. If+-- it is @('Just' y)@, the key @k@ is bound to the new value @y@.+update :: (Binary k, Binary v) => (v -> Maybe v) -> k+       -> Database k v -> Transaction ReadWrite Bool+update f = updateWithKey (const f)++-- | The monadic action @('updateWithKey' f k db)@ updates the value @x@ at+-- @k@ (if it is in the database). If @(f k x)@ is 'Nothing', the element is+-- deleted. If it is @('Just' y)@, the key @k@ is bound to the new value @y@.+updateWithKey :: (Binary k, Binary v) => (k -> v -> Maybe v) -> k+              -> Database k v -> Transaction ReadWrite Bool+updateWithKey f key db = isJust <$> updateLookupWithKey f key db++-- | Lookup and update. See also 'updateWithKey'. The function returns changed+-- value, if it is updated. Returns the original key value if the database+-- entry is deleted.+updateLookupWithKey :: (Binary k, Binary v) => (k -> v -> Maybe v) -> k+                    -> Database k v -> Transaction ReadWrite (Maybe v)+updateLookupWithKey f = alterWithKey (maybe Nothing . f)++-- | The monadic action @('alter' f k db)@ alters the value @x@ at @k@, or+-- absence thereof. 'alter' can be used to insert, delete, or update a value+-- in a database.+alter :: (Binary k, Binary v) => (Maybe v -> Maybe v) -> k+      -> Database k v -> Transaction ReadWrite ()+alter f key db = void $ alterWithKey (const f) key db++alterWithKey :: (Binary k, Binary v) => (k -> Maybe v -> Maybe v) -> k+             -> Database k v -> Transaction ReadWrite (Maybe v)+alterWithKey f key (Db _ dbi) = Txn $ \txn ->+  withCursor txn dbi $ \cursor -> marshalOut key $ \kval ->+  with kval $ \kptr -> alloca $ \vptr -> do+    found <- mdb_cursor_get' MDB_SET cursor kptr vptr+    if found+      then peekVal vptr >>= \oldValue -> do+        let old = Just oldValue+        case f key old of+          new@(Just newValue) -> marshalOut newValue $ \vval ->+            mdb_cursor_put' overwriteFlags cursor kval vval >>+            return new+          Nothing -> mdb_cursor_del' defaultWriteFlags cursor >>+            return old+      else case f key Nothing of+             new@(Just newValue) -> marshalOut newValue $ \vval ->+               mdb_cursor_put' defaultWriteFlags cursor kval vval >>+               return new+             Nothing -> return Nothing
+ src/Database/LMDB/Simple/Internal.hs view
@@ -0,0 +1,230 @@++{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE TypeFamilies #-}++module Database.LMDB.Simple.Internal+  ( ReadWrite+  , ReadOnly+  , Mode+  , SubMode+  , Environment (..)+  , Transaction (..)+  , Database (..)+  , isReadOnlyEnvironment+  , isReadOnlyTransaction+  , isReadWriteTransaction+  , marshalOut+  , marshalIn+  , peekVal+  , forEachForward+  , forEachReverse+  , withCursor+  , defaultWriteFlags+  , overwriteFlags+  , get+  , get'+  , put+  , delete+  ) where++import Control.Exception+  ( assert+  , bracket+  )++import Control.Monad+  ( (>=>)+  , foldM+  )++import Control.Monad.IO.Class+  ( MonadIO (liftIO)+  )++import Data.Binary+  ( Binary+  , encode+  , decode+  )++import Data.ByteString+  ( packCStringLen+  )+import qualified Data.ByteString as BS++import Data.ByteString.Unsafe+  ( unsafeUseAsCStringLen+  )++import Data.ByteString.Lazy+  ( toChunks+  , toStrict+  , fromStrict+  )+import qualified Data.ByteString.Lazy as BSL++import Data.Word+  ( Word8+  )++import Database.LMDB.Raw+  ( MDB_env+  , MDB_txn+  , MDB_dbi'+  , MDB_val (MDB_val)+  , MDB_cursor'+  , MDB_cursor_op (MDB_FIRST, MDB_LAST, MDB_NEXT, MDB_PREV)+  , MDB_WriteFlag (MDB_CURRENT)+  , MDB_WriteFlags+  , mdb_cursor_open'+  , mdb_cursor_close'+  , mdb_cursor_get'+  , mdb_get'+  , mdb_reserve'+  , mdb_del'+  , compileWriteFlags+  )++import Foreign+  ( Ptr+  , castPtr+  , peek+  , plusPtr+  , copyBytes+  )++import GHC.Exts (Constraint)++data ReadWrite+data ReadOnly++class Mode mode where+  isReadOnlyMode :: mode -> Bool++instance Mode ReadWrite where+  isReadOnlyMode _ = False++instance Mode ReadOnly where+  isReadOnlyMode _ = True++type family SubMode a b :: Constraint where+  SubMode a ReadWrite = a ~ ReadWrite+  SubMode a ReadOnly  = ()++-- | An LMDB environment is a directory or file on disk that contains one or+-- more databases, and has an associated (reader) lock table.+newtype Environment mode = Env MDB_env++isReadOnlyEnvironment :: Mode mode => Environment mode -> Bool+isReadOnlyEnvironment = isReadOnlyMode . mode+  where mode :: Environment mode -> mode+        mode = undefined++-- | An LMDB transaction is an atomic unit for reading and/or changing one or+-- more LMDB databases within an environment, during which the transaction has+-- a consistent view of the databases and is unaffected by any other+-- transaction. The effects of a transaction can either be committed to the+-- LMDB environment atomically, or they can be rolled back with no observable+-- effect on the environment if the transaction is aborted.+--+-- Transactions may be 'ReadWrite' or 'ReadOnly', however LMDB enforces a+-- strict single-writer policy so only one top-level 'ReadWrite' transaction+-- may be active at any time.+--+-- This API models transactions using a 'Transaction' monad. This monad has a+-- 'MonadIO' instance so it is possible to perform arbitrary I/O within a+-- transaction using 'liftIO'. However, such 'IO' actions are not atomic and+-- cannot be rolled back if the transaction is aborted, so use with care.+newtype Transaction mode a = Txn (MDB_txn -> IO a)++isReadOnlyTransaction :: Mode mode => Transaction mode a -> Bool+isReadOnlyTransaction = isReadOnlyMode . mode+  where mode :: Transaction mode a -> mode+        mode = undefined++isReadWriteTransaction :: Mode mode => Transaction mode a -> Bool+isReadWriteTransaction = not . isReadOnlyTransaction++instance Functor (Transaction mode) where+  fmap f (Txn tf) = Txn $ \txn -> fmap f (tf txn)++instance Applicative (Transaction mode) where+  pure x = Txn $ \_ -> pure x+  Txn tff <*> Txn tf = Txn $ \txn -> tff txn <*> tf txn++instance Monad (Transaction mode) where+  Txn tf >>= f = Txn $ \txn -> tf txn >>= \r -> let Txn tf' = f r in tf' txn++instance MonadIO (Transaction mode) where+  liftIO io = Txn $ const io++-- | A database maps arbitrary keys to values. This API uses the 'Binary'+-- class to serialize keys and values for LMDB to store on disk.+data Database k v = Db MDB_env MDB_dbi'++peekVal :: Binary v => Ptr MDB_val -> IO v+peekVal = peek >=> marshalIn++marshalIn :: Binary v => MDB_val -> IO v+marshalIn (MDB_val len ptr) =+  decode . fromStrict <$> packCStringLen (castPtr ptr, fromIntegral len)++marshalOut :: Binary v => v -> (MDB_val -> IO a) -> IO a+marshalOut value f =+  unsafeUseAsCStringLen (toStrict $ encode value) $ \(ptr, len) ->+  f $ MDB_val (fromIntegral len) (castPtr ptr)++copyLazyBS :: BSL.ByteString -> Ptr Word8 -> Int -> IO ()+copyLazyBS lbs ptr rem =+  foldM copyBS (ptr, rem) (toChunks lbs) >>= \(_, 0) -> return ()++  where copyBS :: (Ptr Word8, Int) -> BS.ByteString -> IO (Ptr Word8, Int)+        copyBS (ptr, rem) bs = unsafeUseAsCStringLen bs $ \(bsp, len) ->+          assert (len <= rem) $ copyBytes ptr (castPtr bsp) len >>+          return (ptr `plusPtr` len, rem - len)++forEach :: MDB_cursor_op -> MDB_cursor_op+        -> MDB_txn -> MDB_dbi' -> Ptr MDB_val -> Ptr MDB_val+        -> a -> (IO a -> IO a) -> IO a+forEach first next txn dbi kptr vptr acc f =+  withCursor txn dbi $ cursorGet first acc++  where cursorGet op acc cursor = do+          found <- mdb_cursor_get' op cursor kptr vptr+          if found+            then f (cursorGet next acc cursor)+            else pure acc++forEachForward, forEachReverse :: MDB_txn -> MDB_dbi'+                               -> Ptr MDB_val -> Ptr MDB_val+                               -> a -> (IO a -> IO a) -> IO a+forEachForward = forEach MDB_FIRST MDB_NEXT+forEachReverse = forEach MDB_LAST  MDB_PREV++withCursor :: MDB_txn -> MDB_dbi' -> (MDB_cursor' -> IO a) -> IO a+withCursor txn dbi = bracket (mdb_cursor_open' txn dbi) mdb_cursor_close'++defaultWriteFlags, overwriteFlags :: MDB_WriteFlags+defaultWriteFlags = compileWriteFlags []+overwriteFlags    = compileWriteFlags [MDB_CURRENT]++get :: (Binary k, Binary v) => Database k v -> k -> Transaction mode (Maybe v)+get db key = get' db key >>=+  maybe (return Nothing) (liftIO . fmap Just . marshalIn)++get' :: Binary k => Database k v -> k -> Transaction mode (Maybe MDB_val)+get' (Db _ dbi) key = Txn $ \txn -> marshalOut key $ mdb_get' txn dbi++put :: (Binary k, Binary v)+    => Database k v -> k -> v -> Transaction ReadWrite ()+put (Db _ dbi) key value = Txn $ \txn ->+  marshalOut key $ \kval -> do+  let bs = encode value+      sz = fromIntegral (BSL.length bs)+  MDB_val len ptr <- mdb_reserve' defaultWriteFlags txn dbi kval sz+  let len' = fromIntegral len+  assert (len' == sz) $ copyLazyBS bs ptr len'++delete :: Binary k => Database k v -> k -> Transaction ReadWrite Bool+delete (Db _ dbi) key = Txn $ \txn ->+  marshalOut key $ \kval -> mdb_del' txn dbi kval Nothing
+ src/Database/LMDB/Simple/View.hs view
@@ -0,0 +1,227 @@++{-| This module provides a read-only 'View' for an LMDB database at a single+point in time. Because the view is unchanging, it can be used within pure+code. Behind the scenes, data is accessed from the underlying LMDB memory map.++Each 'View' internally keeps open a read-only transaction in the LMDB+environment (consuming a slot in the lock table), so their use should be+minimized and generally short-lived. The transaction should be closed+automatically when the 'View' is garbage collected, but the timing is not+guaranteed.+-}++module Database.LMDB.Simple.View+  ( -- * Creating+    View+  , getView++    -- * Operators+  , (!)+  , (!?)++    -- * Query+  , null+  , size+  , member+  , notMember+  , lookup+  , findWithDefault++    -- * Folds+  , foldr+  , foldl+  , foldrWithKey+  , foldlWithKey+  , foldViewWithKey++    -- * Conversion+  , elems+  , keys+  , toList+  ) where++import Prelude hiding+  ( foldl+  , foldr+  , lookup+  , null+  )++import Control.Concurrent.MVar+  ( MVar+  , newMVar+  , mkWeakMVar+  , takeMVar+  , tryReadMVar+  )++import Control.Monad+  ( (>=>)+  )++import Data.Binary+  ( Binary+  )++import Database.LMDB.Raw+  ( MDB_txn+  , MDB_dbi'+  , mdb_txn_begin+  , mdb_txn_commit+  , mdb_get'+  , mdb_stat'+  , ms_entries+  )++import Database.LMDB.Simple+  ( Database+  )++import Database.LMDB.Simple.Internal+  ( Database (Db)+  , forEachForward+  , forEachReverse+  , marshalOut+  , marshalIn+  , peekVal+  )++import Data.Maybe+  ( fromMaybe+  , isJust+  )++import Foreign+  ( alloca+  , nullPtr+  )++import System.IO.Unsafe+  ( unsafePerformIO+  )++-- | A 'View' behaves much like a 'Data.Map.Map', except in the way it is+-- created. A @'View' k v@ maps keys @k@ to values @v@.+newtype View k v = View (MVar (MDB_txn, MDB_dbi'))++-- | Create and return a read-only 'View' for the given LMDB database.+-- Internally, a read-only transaction is opened and kept alive until the+-- 'View' is garbage collected.+getView :: Database k v -> IO (View k v)+getView (Db env dbi) = do+  txn <- mdb_txn_begin env Nothing True+  var <- newMVar (txn, dbi)+  mkWeakMVar var $ finalize var+  return (View var)++  where finalize :: MVar (MDB_txn, MDB_dbi') -> IO ()+        finalize = takeMVar >=> mdb_txn_commit . fst++{-# NOINLINE viewIO #-}+viewIO :: View k v -> ((MDB_txn, MDB_dbi') -> IO a) -> a+viewIO (View var) f = unsafePerformIO $+  tryReadMVar var >>= maybe (fail "finalized txn") (f >=> seq var . return)++-- | Is the view empty?+null :: View k v -> Bool+null view = viewIO view $ \(txn, dbi) -> do+  stat <- mdb_stat' txn dbi+  return (ms_entries stat == 0)++-- | The number of elements in the view.+size :: View k v -> Int+size view = viewIO view $ \(txn, dbi) -> do+  stat <- mdb_stat' txn dbi+  return (fromIntegral $ ms_entries stat)++-- | Is the key a member of the view? See also 'notMember'.+member :: Binary k => k -> View k v -> Bool+member key view = viewIO view $ \(txn, dbi) ->+  marshalOut key $ \kval -> isJust <$> mdb_get' txn dbi kval++-- | Is the key not a member of the view? See also 'member'.+notMember :: Binary k => k -> View k v -> Bool+notMember key view = not (member key view)++-- | Find the value at a key. Calls 'error' when the element can not be found.+(!) :: (Binary k, Binary v) => View k v -> k -> v+view ! key = fromMaybe notFoundError $ lookup key view+  where notFoundError = error "View.!: given key is not found in the database"+infixl 9 !++-- | Find the value at a key. Returns 'Nothing' when the element can not be found.+(!?) :: (Binary k, Binary v) => View k v -> k -> Maybe v+(!?) = flip lookup+infixl 9 !?++-- | Lookup the value at a key in the view.+--+-- The function will return the corresponding value as @('Just' value)@, or+-- 'Nothing' if the key isn't in the view.+lookup :: (Binary k, Binary v) => k -> View k v -> Maybe v+lookup key view = viewIO view $ \(txn, dbi) -> marshalOut key $+  mdb_get' txn dbi >=> maybe (return Nothing) (fmap Just . marshalIn)++-- | The expression @('findWithDefault' def k view)@ returns the value at key+-- @k@ or returns default value @def@ when the key is not in the view.+findWithDefault :: (Binary k, Binary v) => v -> k -> View k v -> v+findWithDefault def key = fromMaybe def . lookup key++-- | Fold the values in the view using the given right-associative binary+-- operator, such that @'foldr' f z == 'Prelude.foldr' f z . 'elems'@.+foldr :: Binary v => (v -> b -> b) -> b -> View k v -> b+foldr f z view = viewIO view $ \(txn, dbi) ->+  alloca $ \vptr ->+  forEachForward txn dbi nullPtr vptr z $ \rest ->+  f <$> peekVal vptr <*> rest++-- | Fold the keys and values in the view using the given right-associative+-- binary operator, such that @'foldrWithKey' f z == 'Prelude.foldr'+-- ('uncurry' f) z . 'toList'@.+foldrWithKey :: (Binary k, Binary v)+             => (k -> v -> b -> b) -> b -> View k v -> b+foldrWithKey f z view = viewIO view $ \(txn, dbi) ->+  alloca $ \kptr ->+  alloca $ \vptr ->+  forEachForward txn dbi kptr vptr z $ \rest ->+  f <$> peekVal kptr <*> peekVal vptr <*> rest++-- | Fold the values in the view using the given left-associative binary+-- operator, such that @'foldl' f z == 'Prelude.foldl' f z . 'elems'@.+foldl :: Binary v => (a -> v -> a) -> a -> View k v -> a+foldl f z view = viewIO view $ \(txn, dbi) ->+  alloca $ \vptr ->+  forEachReverse txn dbi nullPtr vptr z $ \rest ->+  flip f <$> peekVal vptr <*> rest++-- | Fold the keys and values in the view using the given left-associative+-- binary operator, such that @'foldlWithKey' f z == 'Prelude.foldl' (\\z'+-- (kx, x) -> f z' kx x) z . 'toList'@.+foldlWithKey :: (Binary k, Binary v)+             => (a -> k -> v -> a) -> a -> View k v -> a+foldlWithKey f z view = viewIO view $ \(txn, dbi) ->+  alloca $ \kptr ->+  alloca $ \vptr ->+  forEachReverse txn dbi kptr vptr z $ \rest ->+  (\k v a -> f a k v) <$> peekVal kptr <*> peekVal vptr <*> rest++-- | Fold the keys and values in the view using the given monoid.+foldViewWithKey :: (Monoid m, Binary k, Binary v)+                => (k -> v -> m) -> View k v -> m+foldViewWithKey f = foldrWithKey (\k v a -> f k v `mappend` a) mempty++-- | Return all elements of the view in the order of their keys.+elems :: Binary v => View k v -> [v]+elems = foldr (:) []++-- | Return all keys of the view in the order they are stored in the+-- underlying LMDB database.+keys :: Binary k => View k v -> [k]+keys view = viewIO view $ \(txn, dbi) ->+  alloca $ \kptr ->+  forEachForward txn dbi kptr nullPtr [] $ \rest ->+  (:) <$> peekVal kptr <*> rest++-- | Convert the view to a list of key/value pairs.+toList :: (Binary k, Binary v) => View k v -> [(k, v)]+toList = foldrWithKey (\k v -> ((k, v) :)) []
+ stack.yaml view
@@ -0,0 +1,66 @@+# This file was automatically generated by 'stack init'+#+# Some commonly used options have been documented as comments in this file.+# For advanced use and comprehensive documentation of the format, please see:+# https://docs.haskellstack.org/en/stable/yaml_configuration/++# Resolver to choose a 'specific' stackage snapshot or a compiler version.+# A snapshot resolver dictates the compiler version and the set of packages+# to be used for project dependencies. For example:+#+# resolver: lts-3.5+# resolver: nightly-2015-09-21+# resolver: ghc-7.10.2+# resolver: ghcjs-0.1.0_ghc-7.10.2+# resolver:+#  name: custom-snapshot+#  location: "./custom-snapshot.yaml"+resolver: lts-9.0++# User packages to be built.+# Various formats can be used as shown in the example below.+#+# packages:+# - some-directory+# - https://example.com/foo/bar/baz-0.0.2.tar.gz+# - location:+#    git: https://github.com/commercialhaskell/stack.git+#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a+# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a+#   extra-dep: true+#  subdirs:+#  - auto-update+#  - wai+#+# A package marked 'extra-dep: true' will only be built if demanded by a+# non-dependency (i.e. a user package), and its test suites and benchmarks+# will not be run. This is useful for tweaking upstream packages.+packages:+- .+# Dependency packages to be pulled from upstream that are not in the resolver+# (e.g., acme-missiles-0.3)+extra-deps: []++# Override default flag values for local packages and extra-deps+flags: {}++# Extra package databases containing global packages+extra-package-dbs: []++# Control whether we use the GHC we find on the path+# system-ghc: true+#+# Require a specific version of stack, using version ranges+# require-stack-version: -any # Default+# require-stack-version: ">=1.5"+#+# Override the architecture used by stack, especially useful on Windows+# arch: i386+# arch: x86_64+#+# Extra directories used by stack for building+# extra-include-dirs: [/path/to/dir]+# extra-lib-dirs: [/path/to/dir]+#+# Allow a newer minor version of GHC than the snapshot specifies+# compiler-check: newer-minor
+ test/Database/LMDB/SimpleSpec.hs view
@@ -0,0 +1,54 @@++module Database.LMDB.SimpleSpec+  ( spec+  ) where++import Control.Monad (forM, forM_)+import Database.LMDB.Simple+import Database.LMDB.Simple.Extra+import Harness+import Test.Hspec++spec :: Spec+spec = do+  (env, db) <- runIO setup++  describe "basic operations" $ do+    it "inserts and counts entries" $+      transaction env+      ( do forM_ [1..100] $ \i -> put db i (Just $ show i)+           size db+      ) `shouldReturn` 100++    it "retrieves entries" $+      readOnlyTransaction env (forM [1..100] $ \i -> get db i)+      `shouldReturn` map (Just . show) [1 :: Int .. 100]++    it "deletes entries" $+      transaction env (put db 50 Nothing >> (,) <$> get db 50 <*> size db)+      `shouldReturn` (Nothing, 99)++  describe "transactions" $ do+    it "aborts" $+      transaction env (put db 0 (Just "zero") >> abort)+      `shouldThrow` (const True :: Selector AbortedTransaction)++    it "rolls back" $+      readOnlyTransaction env (get db 0)+      `shouldReturn` Nothing++    it "aborts nested transactions" $+      transaction env+      ( do put db 1 (Just "one")+           nestTransaction $ put db 2 (Just "two") >> abort+      ) `shouldReturn` (Nothing :: Maybe ())++    it "rolls back nested transactions" $+      readOnlyTransaction env ((,) <$> get db 1 <*> get db 2)+      `shouldReturn` (Just "one", Just "2")++    it "commits nested transactions" $+      transaction env+      ( do nestTransaction (put db 3 $ Just "three")+           get db 3+      ) `shouldReturn` Just "three"
+ test/Harness.hs view
@@ -0,0 +1,19 @@++module Harness+  ( setup+  ) where++import Database.LMDB.Simple++setup :: IO (Environment ReadWrite, Database Int String)+setup = do+  env <- openEnvironment "test/env" defaultLimits+         { mapSize      = 1024 * 1024 * 1024+         , maxDatabases = 4+         }+  db <- transaction env $ do+    db <- getDatabase Nothing+    clear db+    return db++  return (env, db)
+ test/criterion.hs view
@@ -0,0 +1,30 @@++module Main where++import Criterion.Main+import Database.LMDB.Simple+import Harness++import Control.Monad (forM, forM_)++main :: IO ()+main = do+  (env, db) <- setup+  defaultMain+    [ bench ("insertion of " ++ elements) $ whnfIO (insertion env db)+    , bench ("retrieval of " ++ elements) $   nfIO (retrieval env db)+    ]++n :: Int+n = 10000++elements :: String+elements = show n ++ " elements"++insertion :: Environment ReadWrite -> Database Int String -> IO ()+insertion env db = transaction env $ do+  clear db+  forM_ [1..n] $ \i -> put db i (Just $ show i)++retrieval :: Environment mode -> Database Int String -> IO [Maybe String]+retrieval env db = readOnlyTransaction env $ forM [1..n] $ \i -> get db i
+ test/env/.keep view
+ test/hspec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/sample.hs view
@@ -0,0 +1,17 @@++module Main where++import Database.LMDB.Simple+import Control.Monad (forM_)++main :: IO ()+main = do+  env <- openEnvironment "test/env" defaultLimits+  db <- readOnlyTransaction env $ getDatabase Nothing :: IO (Database String Int)++  transaction env $ do+    clear db+    forM_ [("one",1),("two",2),("three",3)] $ \(k,v) -> put db k (Just v)++  print =<< readOnlyTransaction env (get db "two")   -- Just 2+  print =<< readOnlyTransaction env (get db "nine")  -- Nothing