mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 22:33:06 +03:00
1445 lines
54 KiB
Haskell
1445 lines
54 KiB
Haskell
{-# LANGUAGE ForeignFunctionInterface, DeriveDataTypeable #-}
|
|
|
|
-- | This module is a thin wrapper above lmdb.h.
|
|
--
|
|
-- Provisions for performance, convenience, or safety:
|
|
--
|
|
-- * Errors are shifted to `LMDB_Error` Haskell exceptions
|
|
-- * flag fields and enums are represented with Haskell types
|
|
-- * MDB_env includes its own write mutex for Haskell's threads
|
|
-- * MDB_RESERVE operations use their own functions
|
|
-- * Databases types are divided for user-defined comparisons
|
|
-- * Boolean-option functions are divided into two functions
|
|
-- * MDB_NOTLS is added implicitly, and may not be removed
|
|
-- * unix mode is set to 0660 (user+group read-write)
|
|
--
|
|
-- Some functions come in two forms based on 'safe' vs. 'unsafe'
|
|
-- FFI bindings. Unsafe FFI bindings are unsuitable for databases
|
|
-- with user-defined comparison operations. (Though, if you plan
|
|
-- to load a database with MDB_APPEND or MDB_APPENDDUP, you can
|
|
-- use an unsafe dbi for just that portion.)
|
|
--
|
|
-- Despite these provisions, developers must still be cautious:
|
|
--
|
|
-- * MDB_val objects are invalid outside their transaction.
|
|
-- * Don't use write operations on a read-only transaction.
|
|
-- * Use 'bound threads' for write transactions.
|
|
--
|
|
-- A slightly higher level API is planned, mostly to provide safer
|
|
-- and more convenient access compared to raw MDB_val objects.
|
|
--
|
|
-- Features not implemented:
|
|
--
|
|
-- * functions directly using file handles
|
|
-- * user-defined relocation functions
|
|
-- * MDB_MULTIPLE is not currently supported (todo)
|
|
--
|
|
module Database.LMDB.Raw
|
|
( LMDB_Version(..), lmdb_version, lmdb_dyn_version
|
|
, LMDB_Error(..), MDB_ErrCode(..)
|
|
|
|
, MDB_env
|
|
, MDB_dbi, MDB_dbi'
|
|
, MDB_txn, MDB_txnid
|
|
, MDB_cursor, MDB_cursor'
|
|
|
|
, MDB_val(..)
|
|
, MDB_stat, ms_psize, ms_depth, ms_branch_pages, ms_leaf_pages, ms_overflow_pages, ms_entries
|
|
, MDB_envinfo, me_mapaddr, me_mapsize, me_last_pgno, me_last_txnid, me_maxreaders, me_numreaders
|
|
, MDB_cmp_func, wrapCmpFn
|
|
, MDB_EnvFlag(..), MDB_DbFlag(..)
|
|
, MDB_cursor_op(..)
|
|
|
|
, MDB_WriteFlag(..), MDB_WriteFlags, compileWriteFlags
|
|
--, MDB_cursor_op(..)
|
|
|
|
-- * Environment Operations
|
|
, mdb_env_create
|
|
, mdb_env_open
|
|
, mdb_env_copy
|
|
, mdb_env_stat
|
|
, mdb_env_info
|
|
, mdb_env_sync, mdb_env_sync_flush
|
|
, mdb_env_close
|
|
, mdb_env_set_flags, mdb_env_unset_flags
|
|
, mdb_env_get_flags
|
|
, mdb_env_get_path
|
|
, mdb_env_set_mapsize
|
|
, mdb_env_set_maxreaders
|
|
, mdb_env_get_maxreaders
|
|
, mdb_env_set_maxdbs
|
|
, mdb_env_get_maxkeysize
|
|
|
|
-- * Transactions
|
|
, mdb_txn_begin
|
|
, mdb_txn_env
|
|
, mdb_txn_commit
|
|
, mdb_txn_abort
|
|
|
|
-- * Databases
|
|
, mdb_dbi_open
|
|
, mdb_stat
|
|
, mdb_dbi_flags
|
|
, mdb_dbi_close
|
|
, mdb_drop, mdb_clear
|
|
, mdb_set_compare
|
|
, mdb_set_dupsort
|
|
|
|
, mdb_dbi_open'
|
|
, mdb_stat'
|
|
, mdb_dbi_flags'
|
|
, mdb_dbi_close'
|
|
, mdb_drop', mdb_clear'
|
|
|
|
-- * Basic Key-Value Access
|
|
, mdb_get, mdb_put, mdb_del, mdb_reserve
|
|
, mdb_get', mdb_put', mdb_del', mdb_reserve'
|
|
|
|
-- * Database key and value Comparisons
|
|
, mdb_cmp, mdb_dcmp
|
|
, mdb_cmp', mdb_dcmp'
|
|
|
|
-- * Cursors
|
|
, mdb_cursor_open
|
|
, mdb_cursor_get
|
|
, mdb_cursor_put
|
|
, mdb_cursor_del
|
|
, mdb_cursor_close
|
|
, mdb_cursor_txn
|
|
, mdb_cursor_dbi
|
|
, mdb_cursor_count
|
|
|
|
, mdb_cursor_open'
|
|
, mdb_cursor_get'
|
|
, mdb_cursor_put'
|
|
, mdb_cursor_del'
|
|
, mdb_cursor_close'
|
|
, mdb_cursor_txn'
|
|
, mdb_cursor_dbi'
|
|
, mdb_cursor_count'
|
|
|
|
-- * Misc
|
|
, mdb_reader_list
|
|
, mdb_reader_check
|
|
|
|
, mdb_txn_reset
|
|
, mdb_txn_renew
|
|
|
|
, withKVPtrs
|
|
, withKVOptPtrs
|
|
) where
|
|
|
|
#include <lmdb.h>
|
|
|
|
import Prelude
|
|
|
|
import Foreign
|
|
import Foreign.C
|
|
import Control.Monad
|
|
import Control.Exception
|
|
import Control.Concurrent
|
|
import qualified Data.Array.Unboxed as A
|
|
import qualified Data.List as L
|
|
import Data.Typeable
|
|
--import System.IO (FilePath)
|
|
import Data.Function (on)
|
|
import Data.Maybe (isNothing)
|
|
import Data.IORef
|
|
|
|
#let alignment t = "%lu", (unsigned long)offsetof(struct {char x__; t (y__); }, y__)
|
|
|
|
-- FFI
|
|
-- 'safe': higher overhead, thread juggling, allows callbacks into Haskell
|
|
-- 'unsafe': lower overhead, reduced concurrency, no callbacks into Haskell
|
|
foreign import ccall unsafe "lmdb.h mdb_version" _mdb_version :: Ptr CInt -> Ptr CInt -> Ptr CInt -> IO CString
|
|
foreign import ccall unsafe "lmdb.h mdb_strerror" _mdb_strerror :: CInt -> CString
|
|
|
|
foreign import ccall "lmdb.h mdb_env_create" _mdb_env_create :: Ptr (Ptr MDB_env) -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_open" _mdb_env_open :: Ptr MDB_env -> CString -> CUInt -> MDB_mode_t -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_copy" _mdb_env_copy :: Ptr MDB_env -> CString -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_stat" _mdb_env_stat :: Ptr MDB_env -> Ptr MDB_stat -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_info" _mdb_env_info :: Ptr MDB_env -> Ptr MDB_envinfo -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_sync" _mdb_env_sync :: Ptr MDB_env -> CInt -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_close" _mdb_env_close :: Ptr MDB_env -> IO ()
|
|
foreign import ccall "lmdb.h mdb_env_set_flags" _mdb_env_set_flags :: Ptr MDB_env -> CUInt -> CInt -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_env_get_flags" _mdb_env_get_flags :: Ptr MDB_env -> Ptr CUInt -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_env_get_path" _mdb_env_get_path :: Ptr MDB_env -> Ptr CString -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_set_mapsize" _mdb_env_set_mapsize :: Ptr MDB_env -> CSize -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_set_maxreaders" _mdb_env_set_maxreaders :: Ptr MDB_env -> CUInt -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_env_get_maxreaders" _mdb_env_get_maxreaders :: Ptr MDB_env -> Ptr CUInt -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_env_set_maxdbs" _mdb_env_set_maxdbs :: Ptr MDB_env -> MDB_dbi_t -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_env_get_maxkeysize" _mdb_env_get_maxkeysize :: Ptr MDB_env -> IO CInt
|
|
|
|
foreign import ccall "lmdb.h mdb_txn_begin" _mdb_txn_begin :: Ptr MDB_env -> Ptr MDB_txn -> CUInt -> Ptr (Ptr MDB_txn) -> IO CInt
|
|
-- foreign import ccall "lmdb.h mdb_txn_env" _mdb_txn_env :: MDB_txn -> IO (Ptr MDB_env)
|
|
foreign import ccall "lmdb.h mdb_txn_commit" _mdb_txn_commit :: Ptr MDB_txn -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_txn_abort" _mdb_txn_abort :: Ptr MDB_txn -> IO ()
|
|
|
|
-- I'm hoping to get a patch adding the following function into LMDB:
|
|
-- foreign import ccall "lmdb.h mdb_txn_id" _mdb_txn_id :: MDB_txn -> IO MDB_txnid_t
|
|
|
|
foreign import ccall "lmdb.h mdb_dbi_open" _mdb_dbi_open :: Ptr MDB_txn -> CString -> CUInt -> Ptr MDB_dbi_t -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_stat" _mdb_stat :: Ptr MDB_txn -> MDB_dbi_t -> Ptr MDB_stat -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_dbi_flags" _mdb_dbi_flags :: Ptr MDB_txn -> MDB_dbi_t -> Ptr CUInt -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_dbi_close" _mdb_dbi_close :: Ptr MDB_env -> MDB_dbi_t -> IO ()
|
|
foreign import ccall "lmdb.h mdb_drop" _mdb_drop :: Ptr MDB_txn -> MDB_dbi_t -> CInt -> IO CInt
|
|
|
|
-- comparisons may only be configured for a 'safe' MDB_dbi.
|
|
foreign import ccall "lmdb.h mdb_set_compare" _mdb_set_compare :: Ptr MDB_txn -> MDB_dbi -> FunPtr MDB_cmp_func -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_set_dupsort" _mdb_set_dupsort :: Ptr MDB_txn -> MDB_dbi -> FunPtr MDB_cmp_func -> IO CInt
|
|
|
|
foreign import ccall safe "lmdb.h mdb_cmp" _mdb_cmp :: Ptr MDB_txn -> MDB_dbi -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_dcmp" _mdb_dcmp :: Ptr MDB_txn -> MDB_dbi -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_cmp" _mdb_cmp' :: Ptr MDB_txn -> MDB_dbi' -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_dcmp" _mdb_dcmp' :: Ptr MDB_txn -> MDB_dbi' -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
|
|
foreign import ccall safe "lmdb.h mdb_get" _mdb_get :: Ptr MDB_txn -> MDB_dbi -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_put" _mdb_put :: Ptr MDB_txn -> MDB_dbi -> Ptr MDB_val -> Ptr MDB_val -> MDB_WriteFlags -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_del" _mdb_del :: Ptr MDB_txn -> MDB_dbi -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_get" _mdb_get' :: Ptr MDB_txn -> MDB_dbi' -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_put" _mdb_put' :: Ptr MDB_txn -> MDB_dbi' -> Ptr MDB_val -> Ptr MDB_val -> MDB_WriteFlags -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_del" _mdb_del' :: Ptr MDB_txn -> MDB_dbi' -> Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
|
|
-- I dislike LMDB's cursor interface: one 'get' function with 18 special cases.
|
|
-- Seems like it should be 18 functions.
|
|
foreign import ccall safe "lmdb.h mdb_cursor_open" _mdb_cursor_open :: Ptr MDB_txn -> MDB_dbi -> Ptr (Ptr MDB_cursor) -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_cursor_close" _mdb_cursor_close :: Ptr MDB_cursor -> IO ()
|
|
foreign import ccall safe "lmdb.h mdb_cursor_get" _mdb_cursor_get :: Ptr MDB_cursor -> Ptr MDB_val -> Ptr MDB_val -> (#type MDB_cursor_op) -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_cursor_put" _mdb_cursor_put :: Ptr MDB_cursor -> Ptr MDB_val -> Ptr MDB_val -> MDB_WriteFlags -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_cursor_del" _mdb_cursor_del :: Ptr MDB_cursor -> MDB_WriteFlags -> IO CInt
|
|
foreign import ccall safe "lmdb.h mdb_cursor_count" _mdb_cursor_count :: Ptr MDB_cursor -> Ptr CSize -> IO CInt
|
|
-- foreign import ccall safe "lmdb.h mdb_cursor_txn" _mdb_cursor_txn :: Ptr MDB_cursor -> IO (Ptr MDB_txn)
|
|
-- foreign import ccall safe "lmdb.h mdb_cursor_dbi" _mdb_cursor_dbi :: Ptr MDB_cursor -> IO MDB_dbi
|
|
|
|
foreign import ccall unsafe "lmdb.h mdb_cursor_open" _mdb_cursor_open' :: Ptr MDB_txn -> MDB_dbi' -> Ptr (Ptr MDB_cursor') -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_cursor_close" _mdb_cursor_close' :: Ptr MDB_cursor' -> IO ()
|
|
foreign import ccall unsafe "lmdb.h mdb_cursor_get" _mdb_cursor_get' :: Ptr MDB_cursor' -> Ptr MDB_val -> Ptr MDB_val -> (#type MDB_cursor_op) -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_cursor_put" _mdb_cursor_put' :: Ptr MDB_cursor' -> Ptr MDB_val -> Ptr MDB_val -> MDB_WriteFlags -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_cursor_del" _mdb_cursor_del' :: Ptr MDB_cursor' -> MDB_WriteFlags -> IO CInt
|
|
foreign import ccall unsafe "lmdb.h mdb_cursor_count" _mdb_cursor_count' :: Ptr MDB_cursor' -> Ptr CSize -> IO CInt
|
|
-- foreign import ccall unsafe "lmdb.h mdb_cursor_txn" _mdb_cursor_txn' :: Ptr MDB_cursor -> IO (Ptr MDB_txn)
|
|
-- foreign import ccall unsafe "lmdb.h mdb_cursor_dbi" _mdb_cursor_dbi' :: Ptr MDB_cursor -> IO MDB_dbi
|
|
|
|
foreign import ccall unsafe "lmdb.h mdb_txn_reset" _mdb_txn_reset :: Ptr MDB_txn -> IO ()
|
|
foreign import ccall "lmdb.h mdb_txn_renew" _mdb_txn_renew :: Ptr MDB_txn -> IO CInt
|
|
|
|
foreign import ccall "lmdb.h mdb_reader_list" _mdb_reader_list :: Ptr MDB_env -> FunPtr MDB_msg_func -> Ptr () -> IO CInt
|
|
foreign import ccall "lmdb.h mdb_reader_check" _mdb_reader_check :: Ptr MDB_env -> Ptr CInt -> IO CInt
|
|
|
|
-- | User-defined comparison functions for keys.
|
|
type MDB_cmp_func = Ptr MDB_val -> Ptr MDB_val -> IO CInt
|
|
foreign import ccall "wrapper" wrapCmpFn :: MDB_cmp_func -> IO (FunPtr MDB_cmp_func)
|
|
|
|
-- callback function for reader list (used internally to this binding)
|
|
type MDB_msg_func = CString -> Ptr () -> IO CInt
|
|
foreign import ccall "wrapper" wrapMsgFunc :: MDB_msg_func -> IO (FunPtr MDB_msg_func)
|
|
|
|
|
|
-- Haskell seems to have difficulty inferring the `Ptr CInt` from
|
|
-- the _mdb_version call. (This seriously annoys me.)
|
|
_peekCInt :: Ptr CInt -> IO CInt
|
|
_peekCInt = peek
|
|
|
|
_peekCUInt :: Ptr CUInt -> IO CUInt
|
|
_peekCUInt = peek
|
|
|
|
-- | Version information for LMDB. Two potentially different versions
|
|
-- can be obtained: lmdb_version returns the version at the time of
|
|
-- binding (via C preprocessor macros) and lmdb_dyn_version returns a
|
|
-- version for the bound library.
|
|
--
|
|
-- These bindings to Haskell will refuse to open the database when
|
|
-- the dynamic version of LMDB is different in the major or minor
|
|
-- fields.
|
|
data LMDB_Version = LMDB_Version
|
|
{ v_major :: {-# UNPACK #-} !Int
|
|
, v_minor :: {-# UNPACK #-} !Int
|
|
, v_patch :: {-# UNPACK #-} !Int
|
|
, v_text :: !String
|
|
} deriving (Eq, Ord, Show)
|
|
|
|
-- | Version of LMDB when the Haskell-LMDB binding was compiled.
|
|
lmdb_version :: LMDB_Version
|
|
lmdb_version = LMDB_Version
|
|
{ v_major = #const MDB_VERSION_MAJOR
|
|
, v_minor = #const MDB_VERSION_MINOR
|
|
, v_patch = #const MDB_VERSION_PATCH
|
|
, v_text = #const_str MDB_VERSION_STRING
|
|
}
|
|
|
|
|
|
-- | Version of LMDB linked to the current Haskell process.
|
|
lmdb_dyn_version :: IO LMDB_Version
|
|
lmdb_dyn_version =
|
|
let szInt = sizeOf (undefined :: CInt) in
|
|
allocaBytes (3 * szInt) $ \ pMajor -> do
|
|
let pMinor = pMajor `plusPtr` szInt
|
|
let pPatch = pMinor `plusPtr` szInt
|
|
cvText <- _mdb_version pMajor pMinor pPatch
|
|
vMajor <- fromIntegral <$> _peekCInt pMajor
|
|
vMinor <- fromIntegral <$> _peekCInt pMinor
|
|
vPatch <- fromIntegral <$> _peekCInt pPatch
|
|
vText <- peekCString cvText
|
|
return $! LMDB_Version
|
|
{ v_major = vMajor
|
|
, v_minor = vMinor
|
|
, v_patch = vPatch
|
|
, v_text = vText
|
|
}
|
|
|
|
-- | LMDB_Error is the exception type thrown in case a function from
|
|
-- the LMDB API does not return successfully. Clients should be
|
|
-- prepared to catch exceptions from any LMDB operation.
|
|
data LMDB_Error = LMDB_Error
|
|
{ e_context :: String
|
|
, e_description :: String
|
|
, e_code :: Either Int MDB_ErrCode
|
|
} deriving (Eq, Ord, Show, Typeable)
|
|
instance Exception LMDB_Error
|
|
|
|
-- | Opaque structure for LMDB environment.
|
|
--
|
|
-- The environment additionally contains an MVar to enforce at most
|
|
-- one lightweight Haskell thread is writing at a time. This is
|
|
-- necessary so long as LMDB uses a long-lived mutex for writes, as
|
|
-- in v0.9.10.
|
|
--
|
|
data MDB_env = MDB_env
|
|
{ _env_ptr :: {-# UNPACK #-} !(Ptr MDB_env) -- opaque pointer to LMDB object
|
|
, _env_wlock :: {-# UNPACK #-} !(MVar ThreadId) -- write lock
|
|
}
|
|
|
|
-- | Opaque structure for LMDB transaction.
|
|
data MDB_txn = MDB_txn
|
|
{ _txn_ptr :: {-# UNPACK #-} !(Ptr MDB_txn)
|
|
, _txn_env :: !MDB_env -- environment that owns this transaction.
|
|
, _txn_rw :: !Bool -- is this a read-write transaction? (vs read-only)
|
|
, _txn_p :: !(Maybe MDB_txn) -- parent transaction, if any
|
|
}
|
|
|
|
-- | Identifier for a transaction.
|
|
newtype MDB_txnid = MDB_txnid { _txnid :: MDB_txnid_t } deriving (Ord, Eq, Show)
|
|
|
|
-- | Handle for a database in the environment.
|
|
newtype MDB_dbi = MDB_dbi { _dbi :: MDB_dbi_t }
|
|
|
|
-- | Opaque structure for LMDB cursor.
|
|
data MDB_cursor = MDB_cursor
|
|
{ _crs_ptr :: {-# UNPACK #-} !(Ptr MDB_cursor)
|
|
, _crs_dbi :: {-# UNPACK #-} !MDB_dbi
|
|
, _crs_txn :: !MDB_txn
|
|
}
|
|
|
|
-- | Handle for a database in the environment.
|
|
--
|
|
-- This variation is associated with 'unsafe' FFI calls, with reduced
|
|
-- overhead but no user-defined comparisons. I expect most code using
|
|
-- LMDB could use this variation.
|
|
newtype MDB_dbi' = MDB_dbi' { _dbi' :: MDB_dbi_t }
|
|
|
|
-- | Opaque structure for a cursor on an MDB_dbi' object. Cursors
|
|
-- in this case also use the 'unsafe' FFI calls.
|
|
data MDB_cursor' = MDB_cursor'
|
|
{ _crs_ptr' :: {-# UNPACK #-} !(Ptr MDB_cursor')
|
|
, _crs_dbi' :: {-# UNPACK #-} !MDB_dbi'
|
|
, _crs_txn' :: !MDB_txn
|
|
}
|
|
|
|
type MDB_mode_t = #type mdb_mode_t
|
|
type MDB_dbi_t = #type MDB_dbi
|
|
type MDB_txnid_t = CSize -- typedef not currently exposed
|
|
|
|
-- | A value stored in the database. Be cautious; committing the
|
|
-- transaction that obtained a value should also invalidate it;
|
|
-- avoid capturing MDB_val in a lazy value. A safe interface
|
|
-- similar to STRef could be provided by another module.
|
|
data MDB_val = MDB_val
|
|
{ mv_size :: {-# UNPACK #-} !CSize
|
|
, mv_data :: {-# UNPACK #-} !(Ptr Word8)
|
|
}
|
|
|
|
data MDB_stat = MDB_stat
|
|
{ ms_psize :: {-# UNPACK #-} !CUInt
|
|
, ms_depth :: {-# UNPACK #-} !CUInt
|
|
, ms_branch_pages :: {-# UNPACK #-} !CSize
|
|
, ms_leaf_pages :: {-# UNPACK #-} !CSize
|
|
, ms_overflow_pages :: {-# UNPACK #-} !CSize
|
|
, ms_entries :: {-# UNPACK #-} !CSize
|
|
} deriving (Eq, Ord, Show)
|
|
|
|
data MDB_envinfo = MDB_envinfo
|
|
{ me_mapaddr :: {-# UNPACK #-} !(Ptr ())
|
|
, me_mapsize :: {-# UNPACK #-} !CSize
|
|
, me_last_pgno :: {-# UNPACK #-} !CSize
|
|
, me_last_txnid :: {-# UNPACK #-} !MDB_txnid
|
|
, me_maxreaders :: {-# UNPACK #-} !CUInt
|
|
, me_numreaders :: {-# UNPACK #-} !CUInt
|
|
} deriving (Eq, Ord, Show)
|
|
|
|
|
|
-- | Environment flags from lmdb.h
|
|
--
|
|
-- Note: MDB_NOTLS is implicit and enforced for this binding.
|
|
data MDB_EnvFlag
|
|
= MDB_FIXEDMAP
|
|
| MDB_NOSUBDIR
|
|
| MDB_NOSYNC
|
|
| MDB_RDONLY
|
|
| MDB_NOMETASYNC
|
|
| MDB_WRITEMAP
|
|
| MDB_MAPASYNC
|
|
-- | MDB_NOTLS
|
|
| MDB_NOLOCK
|
|
| MDB_NORDAHEAD
|
|
| MDB_NOMEMINIT
|
|
deriving (Eq, Ord, Bounded, A.Ix, Show)
|
|
|
|
envFlags :: [(MDB_EnvFlag, Int)]
|
|
envFlags =
|
|
[(MDB_FIXEDMAP, #const MDB_FIXEDMAP)
|
|
,(MDB_NOSUBDIR, #const MDB_NOSUBDIR)
|
|
,(MDB_NOSYNC, #const MDB_NOSYNC)
|
|
,(MDB_RDONLY, #const MDB_RDONLY)
|
|
,(MDB_NOMETASYNC, #const MDB_NOMETASYNC)
|
|
,(MDB_WRITEMAP, #const MDB_WRITEMAP)
|
|
,(MDB_MAPASYNC, #const MDB_MAPASYNC)
|
|
-- ,(MDB_NOTLS, #const MDB_NOTLS)
|
|
,(MDB_NOLOCK, #const MDB_NOLOCK)
|
|
,(MDB_NORDAHEAD, #const MDB_NORDAHEAD)
|
|
,(MDB_NOMEMINIT, #const MDB_NOMEMINIT)
|
|
]
|
|
|
|
envFlagsArray :: A.UArray MDB_EnvFlag Int
|
|
envFlagsArray = A.accumArray (.|.) 0 (minBound, maxBound) envFlags
|
|
|
|
compileEnvFlags :: [MDB_EnvFlag] -> CUInt
|
|
compileEnvFlags = fromIntegral . L.foldl' (.|.) 0 . fmap ((A.!) envFlagsArray)
|
|
|
|
decompileBitFlags :: [(a,Int)] -> Int -> [a]
|
|
decompileBitFlags optFlags n = fmap fst $ L.filter fullMatch optFlags where
|
|
fullMatch (_,f) = (f == (n .&. f))
|
|
|
|
decompileEnvFlags :: CUInt -> [MDB_EnvFlag]
|
|
decompileEnvFlags = decompileBitFlags envFlags . fromIntegral
|
|
|
|
|
|
data MDB_DbFlag
|
|
= MDB_REVERSEKEY
|
|
| MDB_DUPSORT
|
|
| MDB_INTEGERKEY
|
|
| MDB_DUPFIXED
|
|
| MDB_INTEGERDUP
|
|
| MDB_REVERSEDUP
|
|
| MDB_CREATE
|
|
deriving (Eq, Ord, Bounded, A.Ix, Show)
|
|
|
|
dbFlags :: [(MDB_DbFlag, Int)]
|
|
dbFlags =
|
|
[(MDB_REVERSEKEY, #const MDB_REVERSEKEY)
|
|
,(MDB_DUPSORT, #const MDB_DUPSORT)
|
|
,(MDB_INTEGERKEY, #const MDB_INTEGERKEY)
|
|
,(MDB_DUPFIXED, #const MDB_DUPFIXED)
|
|
,(MDB_INTEGERDUP, #const MDB_INTEGERDUP)
|
|
,(MDB_REVERSEDUP, #const MDB_REVERSEDUP)
|
|
,(MDB_CREATE, #const MDB_CREATE)
|
|
]
|
|
|
|
dbFlagsArray :: A.UArray MDB_DbFlag Int
|
|
dbFlagsArray = A.accumArray (.|.) 0 (minBound,maxBound) dbFlags
|
|
|
|
compileDBFlags :: [MDB_DbFlag] -> CUInt
|
|
compileDBFlags = fromIntegral . L.foldl' (.|.) 0 . fmap ((A.!) dbFlagsArray)
|
|
|
|
decompileDBFlags :: CUInt -> [MDB_DbFlag]
|
|
decompileDBFlags = decompileBitFlags dbFlags . fromIntegral
|
|
|
|
data MDB_WriteFlag
|
|
= MDB_NOOVERWRITE
|
|
| MDB_NODUPDATA
|
|
| MDB_CURRENT
|
|
-- | MDB_RESERVE -- (needs dedicated function)
|
|
| MDB_APPEND
|
|
| MDB_APPENDDUP
|
|
-- | MDB_MULTIPLE -- (needs special handling)
|
|
deriving (Eq, Ord, Bounded, A.Ix, Show)
|
|
|
|
|
|
writeFlags :: [(MDB_WriteFlag, Int)]
|
|
writeFlags =
|
|
[(MDB_NOOVERWRITE, #const MDB_NOOVERWRITE)
|
|
,(MDB_NODUPDATA, #const MDB_NODUPDATA)
|
|
,(MDB_CURRENT, #const MDB_CURRENT)
|
|
-- ,(MDB_RESERVE, #const MDB_RESERVE)
|
|
,(MDB_APPEND, #const MDB_APPEND)
|
|
,(MDB_APPENDDUP, #const MDB_APPENDDUP)
|
|
-- ,(MDB_MULTIPLE, #const MDB_MULTIPLE)
|
|
]
|
|
|
|
writeFlagsArray :: A.UArray MDB_WriteFlag Int
|
|
writeFlagsArray = A.accumArray (.|.) 0 (minBound,maxBound) writeFlags
|
|
|
|
-- | compiled write flags, corresponding to a [WriteFlag] list. Used
|
|
-- because writes are frequent enough that we want to avoid building
|
|
-- from a list on a per-write basis.
|
|
newtype MDB_WriteFlags = MDB_WriteFlags CUInt
|
|
|
|
-- | compile a list of write flags.
|
|
compileWriteFlags :: [MDB_WriteFlag] -> MDB_WriteFlags
|
|
compileWriteFlags = MDB_WriteFlags . L.foldl' addWF 0 where
|
|
addWF n wf = n .|. fromIntegral (writeFlagsArray A.! wf)
|
|
|
|
data MDB_cursor_op
|
|
= MDB_FIRST
|
|
| MDB_FIRST_DUP
|
|
| MDB_GET_BOTH
|
|
| MDB_GET_BOTH_RANGE
|
|
| MDB_GET_CURRENT
|
|
| MDB_GET_MULTIPLE
|
|
| MDB_LAST
|
|
| MDB_LAST_DUP
|
|
| MDB_NEXT
|
|
| MDB_NEXT_DUP
|
|
| MDB_NEXT_MULTIPLE
|
|
| MDB_NEXT_NODUP
|
|
| MDB_PREV
|
|
| MDB_PREV_DUP
|
|
| MDB_PREV_NODUP
|
|
| MDB_SET
|
|
| MDB_SET_KEY
|
|
| MDB_SET_RANGE
|
|
deriving (Eq, Ord, Bounded, A.Ix, Show)
|
|
|
|
cursorOps :: [(MDB_cursor_op, Int)]
|
|
cursorOps =
|
|
[(MDB_FIRST, #const MDB_FIRST)
|
|
,(MDB_FIRST_DUP, #const MDB_FIRST_DUP)
|
|
,(MDB_GET_BOTH, #const MDB_GET_BOTH)
|
|
,(MDB_GET_BOTH_RANGE, #const MDB_GET_BOTH_RANGE)
|
|
,(MDB_GET_CURRENT, #const MDB_GET_CURRENT)
|
|
,(MDB_GET_MULTIPLE, #const MDB_GET_MULTIPLE)
|
|
,(MDB_LAST, #const MDB_LAST)
|
|
,(MDB_LAST_DUP, #const MDB_LAST_DUP)
|
|
,(MDB_NEXT, #const MDB_NEXT)
|
|
,(MDB_NEXT_DUP, #const MDB_NEXT_DUP)
|
|
,(MDB_NEXT_MULTIPLE, #const MDB_NEXT_MULTIPLE)
|
|
,(MDB_NEXT_NODUP, #const MDB_NEXT_NODUP)
|
|
,(MDB_PREV, #const MDB_PREV)
|
|
,(MDB_PREV_DUP, #const MDB_PREV_DUP)
|
|
,(MDB_PREV_NODUP, #const MDB_PREV_NODUP)
|
|
,(MDB_SET, #const MDB_SET)
|
|
,(MDB_SET_KEY, #const MDB_SET_KEY)
|
|
,(MDB_SET_RANGE, #const MDB_SET_RANGE)
|
|
]
|
|
|
|
cursorOpsArray :: A.UArray MDB_cursor_op Int
|
|
cursorOpsArray = A.accumArray (flip const) minBound (minBound,maxBound) cursorOps
|
|
|
|
cursorOp :: MDB_cursor_op -> (#type MDB_cursor_op)
|
|
cursorOp = fromIntegral . (A.!) cursorOpsArray
|
|
|
|
-- | Error codes from MDB. Note, however, that this API for MDB will mostly
|
|
-- use exceptions for any non-successful return codes. This is mostly included
|
|
-- because I feel the binding would be incomplete otherwise.
|
|
--
|
|
-- (The MDB_SUCCESS return value is excluded.)
|
|
data MDB_ErrCode
|
|
= MDB_KEYEXIST
|
|
| MDB_NOTFOUND
|
|
| MDB_PAGE_NOTFOUND
|
|
| MDB_CORRUPTED
|
|
| MDB_PANIC
|
|
| MDB_VERSION_MISMATCH
|
|
| MDB_INVALID
|
|
| MDB_MAP_FULL
|
|
| MDB_DBS_FULL
|
|
| MDB_READERS_FULL
|
|
| MDB_TLS_FULL
|
|
| MDB_TXN_FULL
|
|
| MDB_CURSOR_FULL
|
|
| MDB_PAGE_FULL
|
|
| MDB_MAP_RESIZED
|
|
| MDB_INCOMPATIBLE
|
|
| MDB_BAD_RSLOT
|
|
| MDB_BAD_TXN
|
|
| MDB_BAD_VALSIZE
|
|
deriving (Eq, Ord, Bounded, A.Ix, Show)
|
|
|
|
errCodes :: [(MDB_ErrCode, Int)]
|
|
errCodes =
|
|
[(MDB_KEYEXIST, #const MDB_KEYEXIST)
|
|
,(MDB_NOTFOUND, #const MDB_NOTFOUND)
|
|
,(MDB_PAGE_NOTFOUND, #const MDB_PAGE_NOTFOUND)
|
|
,(MDB_CORRUPTED, #const MDB_CORRUPTED)
|
|
,(MDB_PANIC, #const MDB_PANIC)
|
|
,(MDB_VERSION_MISMATCH, #const MDB_VERSION_MISMATCH)
|
|
,(MDB_INVALID, #const MDB_INVALID)
|
|
,(MDB_MAP_FULL, #const MDB_MAP_FULL)
|
|
,(MDB_DBS_FULL, #const MDB_DBS_FULL)
|
|
,(MDB_READERS_FULL, #const MDB_READERS_FULL)
|
|
,(MDB_TLS_FULL, #const MDB_TLS_FULL)
|
|
,(MDB_TXN_FULL, #const MDB_TXN_FULL)
|
|
,(MDB_CURSOR_FULL, #const MDB_CURSOR_FULL)
|
|
,(MDB_PAGE_FULL, #const MDB_PAGE_FULL)
|
|
,(MDB_MAP_RESIZED, #const MDB_MAP_RESIZED)
|
|
,(MDB_INCOMPATIBLE, #const MDB_INCOMPATIBLE)
|
|
,(MDB_BAD_RSLOT, #const MDB_BAD_RSLOT)
|
|
,(MDB_BAD_TXN, #const MDB_BAD_TXN)
|
|
,(MDB_BAD_VALSIZE, #const MDB_BAD_VALSIZE)
|
|
]
|
|
|
|
_numToErrVal :: Int -> Either Int MDB_ErrCode
|
|
_numToErrVal code =
|
|
case L.find ((== code) . snd) errCodes of
|
|
Nothing -> Left code
|
|
Just (ec,_) -> Right ec
|
|
|
|
_throwLMDBErrNum :: String -> CInt -> IO noReturn
|
|
_throwLMDBErrNum context errNum = do
|
|
desc <- peekCString (_mdb_strerror errNum)
|
|
throwIO $! LMDB_Error
|
|
{ e_context = context
|
|
, e_description = desc
|
|
, e_code = _numToErrVal (fromIntegral errNum)
|
|
}
|
|
{-# NOINLINE _throwLMDBErrNum #-}
|
|
|
|
-- | Allocate an environment object. This doesn't open the environment.
|
|
--
|
|
-- After creation, but before opening, please use:
|
|
--
|
|
-- mdb_env_set_mapsize
|
|
-- mdb_env_set_maxreaders
|
|
-- mdb_env_set_maxdbs
|
|
--
|
|
-- Then, just after opening, you should create a transaction to open
|
|
-- all the databases (MDB_dbi and MDB_dbi' values) your application
|
|
-- will use.
|
|
--
|
|
-- The typical use case would then involve keeping all these open
|
|
-- until your application either shuts down or crashes.
|
|
--
|
|
-- In addition to normal LMDB errors, this operation may throw an
|
|
-- MDB_VERSION_MISMATCH if the Haskell LMDB bindings don't match
|
|
-- the dynamic version. If this happens, you'll need to rebuild the
|
|
-- lmdb Haskell package.
|
|
mdb_env_create :: IO MDB_env
|
|
mdb_env_create = alloca $ \ ppEnv ->
|
|
lmdb_validate_version_match >>
|
|
_mdb_env_create ppEnv >>= \ rc ->
|
|
if (0 /= rc) then _throwLMDBErrNum "mdb_env_create" rc else
|
|
MDB_env <$> peek ppEnv <*> newEmptyMVar
|
|
|
|
|
|
lmdb_validate_version_match :: IO ()
|
|
lmdb_validate_version_match =
|
|
let vStat = lmdb_version in
|
|
lmdb_dyn_version >>= \ vDyn ->
|
|
unless (versionMatch vStat vDyn) $
|
|
throwIO $! LMDB_Error
|
|
{ e_context = "lmdb_validate_version_match"
|
|
, e_description = "Haskell bindings: " ++ show vStat
|
|
++ "\tDynamic library: " ++ show vDyn
|
|
, e_code = Right MDB_VERSION_MISMATCH
|
|
}
|
|
|
|
-- this match function is a bit relaxed, e.g. it will accept
|
|
-- LMDB 0.9.10 with 0.9.8, so long as the first two numbers
|
|
-- match.
|
|
versionMatch :: LMDB_Version -> LMDB_Version -> Bool
|
|
versionMatch vA vB = matchMajor && matchMinor where
|
|
matchMajor = ((==) `on` v_major) vA vB
|
|
matchMinor = ((==) `on` v_minor) vA vB
|
|
|
|
-- | open or build a database in the filesystem. The named directory
|
|
-- must already exist and be writeable. Before opening, be sure to
|
|
-- at least apply `mdb_env_set_mapsize`.
|
|
--
|
|
-- After opening the environment, you should open the databases:
|
|
--
|
|
-- Create the environment.
|
|
-- Open a transaction.
|
|
-- Open all DBI handles the app will need.
|
|
-- Commit the transaction.
|
|
-- Use those DBI handles in subsequent transactions
|
|
--
|
|
mdb_env_open :: MDB_env -> FilePath -> [MDB_EnvFlag] -> IO ()
|
|
mdb_env_open env fp flags =
|
|
let iFlags = (#const MDB_NOTLS) .|. (compileEnvFlags flags) in
|
|
let unix_mode = (6 * 64 + 6 * 8) in -- mode 0660, read-write for user+group
|
|
withCString fp $ \ cfp ->
|
|
_mdb_env_open (_env_ptr env) cfp iFlags unix_mode >>= \ rc ->
|
|
unless (0 == rc) $
|
|
_throwLMDBErrNum "mdb_env_open" rc
|
|
|
|
-- | Copy the environment to an empty (but existing) directory.
|
|
--
|
|
-- Note: the LMDB copy operation temporarily grabs the writer mutex.
|
|
-- Unfortunately, this greatly complicates the binding to Haskell.
|
|
-- This interface, mdb_env_copy, conservatively blocks all writers
|
|
-- in the same process for the entire duration of copy.
|
|
--
|
|
-- Recommendation: Don't use this function in the same process with
|
|
-- writers. Consider use of the `mdb_copy` command line utility if
|
|
-- you need hot copies.
|
|
--
|
|
mdb_env_copy :: MDB_env -> FilePath -> IO ()
|
|
mdb_env_copy env fp =
|
|
runInBoundThread $
|
|
bracket_ (_lockEnv env) (_unlockEnv env) $
|
|
withCString fp $ \ cfp ->
|
|
_mdb_env_copy (_env_ptr env) cfp >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_env_copy" rc)
|
|
|
|
|
|
-- | obtain statistics for environment
|
|
mdb_env_stat :: MDB_env -> IO MDB_stat
|
|
mdb_env_stat env =
|
|
alloca $ \ pStats ->
|
|
_mdb_env_stat (_env_ptr env) pStats >>= \ rc ->
|
|
if (0 == rc) then peek pStats else
|
|
_throwLMDBErrNum "mdb_env_stat" rc
|
|
|
|
-- | obtain ad-hoc information about the environment.
|
|
mdb_env_info :: MDB_env -> IO MDB_envinfo
|
|
mdb_env_info env =
|
|
alloca $ \ pInfo ->
|
|
_mdb_env_info (_env_ptr env) pInfo >>= \ rc ->
|
|
if (0 == rc) then peek pInfo else
|
|
_throwLMDBErrNum "mdb_env_info" rc
|
|
|
|
-- | Initiate synchronization of environment with disk. However, if
|
|
-- the MDB_NOSYNC or MDB_MAPASYNC flags are active, this won't wait
|
|
-- for the operation to finish. Cf. mdb_env_sync_flush.
|
|
mdb_env_sync :: MDB_env -> IO ()
|
|
mdb_env_sync env =
|
|
_mdb_env_sync (_env_ptr env) 0 >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_env_sync" rc)
|
|
|
|
-- | Force buffered writes to disk before returning.
|
|
mdb_env_sync_flush :: MDB_env -> IO ()
|
|
mdb_env_sync_flush env =
|
|
_mdb_env_sync (_env_ptr env) 1 >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_env_sync_flush" rc)
|
|
|
|
-- | Close the environment. The MDB_env object should not be used by
|
|
-- any operations during or after closing.
|
|
mdb_env_close :: MDB_env -> IO ()
|
|
mdb_env_close env = _lockEnv env >> _mdb_env_close (_env_ptr env)
|
|
|
|
-- | Set flags for the environment.
|
|
mdb_env_set_flags :: MDB_env -> [MDB_EnvFlag] -> IO ()
|
|
mdb_env_set_flags env flags =
|
|
_mdb_env_set_flags (_env_ptr env) (compileEnvFlags flags) 1 >>= \ rc ->
|
|
unless (0 == rc) $ _throwLMDBErrNum "mdb_env_set_flags" rc
|
|
|
|
-- | Unset flags for the environment.
|
|
mdb_env_unset_flags :: MDB_env -> [MDB_EnvFlag] -> IO ()
|
|
mdb_env_unset_flags env flags =
|
|
_mdb_env_set_flags (_env_ptr env) (compileEnvFlags flags) 0 >>= \ rc ->
|
|
unless (0 == rc) $ _throwLMDBErrNum "mdb_env_unset_flags" rc
|
|
|
|
-- | View the current set of flags for the environment.
|
|
mdb_env_get_flags :: MDB_env -> IO [MDB_EnvFlag]
|
|
mdb_env_get_flags env = decompileEnvFlags <$> _mdb_env_get_flags_u env
|
|
|
|
_mdb_env_get_flags_u :: MDB_env -> IO CUInt
|
|
_mdb_env_get_flags_u env = alloca $ \ pFlags ->
|
|
_mdb_env_get_flags (_env_ptr env) pFlags >>= \ rc ->
|
|
if (0 == rc) then peek pFlags else
|
|
_throwLMDBErrNum "mdb_env_get_flags" rc
|
|
|
|
-- | Obtain filesystem path for this environment.
|
|
mdb_env_get_path :: MDB_env -> IO FilePath
|
|
mdb_env_get_path env = alloca $ \ pPathStr ->
|
|
_mdb_env_get_path (_env_ptr env) pPathStr >>= \ rc ->
|
|
if (0 == rc) then peekCString =<< peek pPathStr else
|
|
_throwLMDBErrNum "mdb_env_get_path" rc
|
|
|
|
-- | Set the memory map size, in bytes, for this environment. This
|
|
-- determines the maximum size for the environment and databases,
|
|
-- but typically only a small fraction of the database is in memory
|
|
-- at any given moment.
|
|
--
|
|
-- It is not a problem to set this to a very large number, hundreds
|
|
-- of gigabytes or even terabytes, assuming a sufficiently large
|
|
-- address space. It should be set to a multiple of page size.
|
|
--
|
|
-- The default map size is 1MB, intentionally set low to force
|
|
-- developers to select something larger.
|
|
mdb_env_set_mapsize :: MDB_env -> Int -> IO ()
|
|
mdb_env_set_mapsize env nBytes =
|
|
_mdb_env_set_mapsize (_env_ptr env) (fromIntegral nBytes) >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_env_set_mapsize" rc)
|
|
|
|
-- | Set the maximum number of concurrent readers.
|
|
mdb_env_set_maxreaders :: MDB_env -> Int -> IO ()
|
|
mdb_env_set_maxreaders env nReaders =
|
|
_mdb_env_set_maxreaders (_env_ptr env) (fromIntegral nReaders) >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_env_set_maxreaders" rc)
|
|
|
|
-- | Get the maximum number of concurrent readers.
|
|
mdb_env_get_maxreaders :: MDB_env -> IO Int
|
|
mdb_env_get_maxreaders env = alloca $ \ pCount ->
|
|
_mdb_env_get_maxreaders (_env_ptr env) pCount >>= \ rc ->
|
|
if (0 == rc) then fromIntegral <$> _peekCUInt pCount else
|
|
_throwLMDBErrNum "mdb_env_get_maxreaders" rc
|
|
|
|
-- | Set the maximum number of named databases. LMDB is designed to
|
|
-- support a small handful of databases.
|
|
mdb_env_set_maxdbs :: MDB_env -> Int -> IO ()
|
|
mdb_env_set_maxdbs env nDBs =
|
|
_mdb_env_set_maxdbs (_env_ptr env) (fromIntegral nDBs) >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_env_set_maxdbs" rc)
|
|
|
|
-- | Key sizes in LMDB are determined by a compile-time constant,
|
|
-- defaulting to 511 bytes. This function returns the maximum.
|
|
mdb_env_get_maxkeysize :: MDB_env -> IO Int
|
|
mdb_env_get_maxkeysize env = fromIntegral <$> _mdb_env_get_maxkeysize (_env_ptr env)
|
|
|
|
-- | Check for stale readers, and return number of stale readers cleared.
|
|
mdb_reader_check :: MDB_env -> IO Int
|
|
mdb_reader_check env =
|
|
alloca $ \ pCount ->
|
|
_mdb_reader_check (_env_ptr env) pCount >>= \ rc ->
|
|
if (0 == rc) then fromIntegral <$> _peekCInt pCount else
|
|
_throwLMDBErrNum "mdb_reader_check" rc
|
|
|
|
-- | Dump entries from reader lock table (for human consumption)
|
|
mdb_reader_list :: MDB_env -> IO String
|
|
mdb_reader_list env =
|
|
newIORef [] >>= \ rf ->
|
|
let onMsg cs _ =
|
|
peekCString cs >>= \ msg ->
|
|
modifyIORef rf (msg:) >>
|
|
return 0
|
|
in
|
|
withMsgFunc onMsg $ \ pMsgFunc ->
|
|
_mdb_reader_list (_env_ptr env) pMsgFunc nullPtr >>= \ rc ->
|
|
let toMsg = L.foldl (flip (++)) [] in
|
|
if (0 == rc) then toMsg <$> readIORef rf else
|
|
_throwLMDBErrNum "mdb_reader_list" rc
|
|
|
|
withMsgFunc :: MDB_msg_func -> (FunPtr MDB_msg_func -> IO a) -> IO a
|
|
withMsgFunc f = bracket (wrapMsgFunc f) freeHaskellFunPtr
|
|
|
|
-- | Begin a new transaction, possibly read-only, with a possible parent.
|
|
--
|
|
-- mdb_txn_begin env parent bReadOnly
|
|
--
|
|
-- NOTE: Unless your MDB_env was created with MDB_NOLOCK, it is necessary
|
|
-- that read-write transactions be created and completed in one Haskell
|
|
-- 'bound' thread, e.g. via forkOS or runInBoundThread. The bound threads
|
|
-- are necessary because LMDB uses OS-level mutexes which track the thread
|
|
-- ID of their owning thread.
|
|
--
|
|
-- This LMDB adapter includes its own MVar mutex to prevent more than one
|
|
-- Haskell-level thread from trying to write at the same time.
|
|
--
|
|
-- The hierarchical transactions are useful for read-write transactions.
|
|
-- They allow trying something out then aborting if it doesn't work. But
|
|
-- only one child should be active at a time, all in the same OS thread.
|
|
--
|
|
mdb_txn_begin :: MDB_env -> Maybe MDB_txn -> Bool -> IO MDB_txn
|
|
mdb_txn_begin env parent bReadOnly = mask_ $
|
|
let bWriteTxn = not bReadOnly in
|
|
let bLockForWrite = bWriteTxn && isNothing parent in
|
|
|
|
-- allow only one toplevel write operation at a time.
|
|
when bLockForWrite (_lockEnv env) >>
|
|
let pEnv = _env_ptr env in
|
|
let pParent = maybe nullPtr _txn_ptr parent in
|
|
let iFlags = if bReadOnly then (#const MDB_RDONLY) else 0 in
|
|
let onFailure rc =
|
|
when bLockForWrite (_unlockEnv env) >>
|
|
_throwLMDBErrNum "mdb_txn_begin" rc
|
|
in
|
|
alloca $ \ ppChildTxn ->
|
|
_mdb_txn_begin pEnv pParent iFlags ppChildTxn >>= \ rc ->
|
|
if (0 /= rc) then onFailure rc else
|
|
peek ppChildTxn >>= \ pChildTxn ->
|
|
return $! MDB_txn { _txn_ptr = pChildTxn
|
|
, _txn_env = env
|
|
, _txn_rw = bWriteTxn
|
|
, _txn_p = parent
|
|
}
|
|
|
|
-- Haskell-level writer lock for the environment.
|
|
--
|
|
-- This is necessary because otherwise multiple Haskell threads on
|
|
-- the same OS thread can acquire the same lock (in windows) or
|
|
-- deadlock (in posix systems). LMDB doesn't really support M:N
|
|
-- writer threads.
|
|
--
|
|
-- Note: this will also enforce that the caller is operating in a
|
|
-- bound thread. So we can only _lockEnv from a bound thread, and
|
|
-- we can only _unlockEnv from the same thread.
|
|
_lockEnv, _unlockEnv :: MDB_env -> IO ()
|
|
_lockErr, _unlockErr :: LMDB_Error
|
|
_lockErr = LMDB_Error
|
|
{ e_context = "locking LMDB for write in Haskell layer"
|
|
, e_description = "must lock from a 'bound' thread!"
|
|
, e_code = Right MDB_PANIC
|
|
}
|
|
_unlockErr = LMDB_Error
|
|
{ e_context = "unlock Haskell layer LMDB after write"
|
|
, e_description = "calling thread does not own the lock!"
|
|
, e_code = Right MDB_PANIC
|
|
}
|
|
|
|
_lockEnv env = do
|
|
safeThread <- isCurrentThreadBound <||> isUnlockedEnv
|
|
unless safeThread (throwIO _lockErr)
|
|
tid <- myThreadId
|
|
putMVar (_env_wlock env) tid
|
|
where
|
|
isUnlockedEnv = hasFlag (#const MDB_NOLOCK) <$> _mdb_env_get_flags_u env
|
|
hasFlag f fs = (f == (f .&. fs))
|
|
getA <||> getB = getA >>= \ a -> if a then return True else getB
|
|
|
|
_unlockEnv env =
|
|
myThreadId >>= \ self ->
|
|
let m = (_env_wlock env) in
|
|
mask_ $
|
|
takeMVar m >>= \ owner ->
|
|
unless (self == owner) $
|
|
putMVar m owner >> -- oops!
|
|
throwIO _unlockErr
|
|
|
|
-- compute whether this transaction should hold the write lock.
|
|
-- If it does, unlock it. Otherwise, return.
|
|
_unlockTxn :: MDB_txn -> IO ()
|
|
_unlockTxn txn =
|
|
let bHasLock = _txn_rw txn && isNothing (_txn_p txn) in
|
|
when bHasLock (_unlockEnv (_txn_env txn))
|
|
|
|
-- | Access environment for a transaction.
|
|
mdb_txn_env :: MDB_txn -> MDB_env
|
|
mdb_txn_env = _txn_env
|
|
|
|
-- | Commit a transaction. Don't use the transaction after this.
|
|
mdb_txn_commit :: MDB_txn -> IO ()
|
|
mdb_txn_commit txn = mask_ $
|
|
_mdb_txn_commit (_txn_ptr txn) >>= \ rc ->
|
|
_unlockTxn txn >>
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_txn_commit" rc)
|
|
|
|
-- | Abort a transaction. Don't use the transaction after this.
|
|
mdb_txn_abort :: MDB_txn -> IO ()
|
|
mdb_txn_abort txn = mask_ $
|
|
_mdb_txn_abort (_txn_ptr txn) >>
|
|
_unlockTxn txn
|
|
|
|
-- | Abort a read-only transaction, but don't destroy it.
|
|
-- Keep it available for mdb_txn_renew.
|
|
mdb_txn_reset :: MDB_txn -> IO ()
|
|
mdb_txn_reset txn = _mdb_txn_reset (_txn_ptr txn)
|
|
|
|
-- | Renew a read-only transaction that was previously _reset.
|
|
mdb_txn_renew :: MDB_txn -> IO ()
|
|
mdb_txn_renew txn =
|
|
_mdb_txn_renew (_txn_ptr txn) >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_txn_renew" rc)
|
|
|
|
|
|
{-
|
|
|
|
-- I'm hoping to get a patch adding the following function into the main LMDB:
|
|
-- foreign import ccall "lmdb.h mdb_txn_id" _mdb_txn_id :: MDB_txn -> IO MDB_txnid_t
|
|
|
|
-}
|
|
|
|
-- | Open a database that supports user-defined comparisons, but
|
|
-- has slightly more FFI overhead for reads and writes.
|
|
--
|
|
-- LMDB supports a small set of named databases, plus one 'main'
|
|
-- database using the null argument for the database name.
|
|
mdb_dbi_open :: MDB_txn -> Maybe String -> [MDB_DbFlag] -> IO MDB_dbi
|
|
mdb_dbi_open txn dbName flags = MDB_dbi <$> mdb_dbi_open_t txn dbName flags
|
|
|
|
-- | database statistics
|
|
mdb_stat :: MDB_txn -> MDB_dbi -> IO MDB_stat
|
|
mdb_stat txn = mdb_stat_t txn . _dbi
|
|
|
|
-- | review flags from database
|
|
mdb_dbi_flags :: MDB_txn -> MDB_dbi -> IO [MDB_DbFlag]
|
|
mdb_dbi_flags txn = mdb_dbi_flags_t txn . _dbi
|
|
|
|
-- | close the database handle.
|
|
--
|
|
-- Note: the normal use-case for LMDB is to open all the database
|
|
-- handles up front, then hold onto them until the application is
|
|
-- closed or crashed. In that case, you don't need to bother with
|
|
-- closing database handles.
|
|
mdb_dbi_close :: MDB_env -> MDB_dbi -> IO ()
|
|
mdb_dbi_close env = mdb_dbi_close_t env . _dbi
|
|
|
|
-- | remove the database and close the handle; don't use MDB_dbi
|
|
-- after this
|
|
mdb_drop :: MDB_txn -> MDB_dbi -> IO ()
|
|
mdb_drop txn = mdb_drop_t txn . _dbi
|
|
|
|
-- | clear contents of database, reset to empty
|
|
mdb_clear :: MDB_txn -> MDB_dbi -> IO ()
|
|
mdb_clear txn = mdb_clear_t txn . _dbi
|
|
|
|
mdb_dbi_open' :: MDB_txn -> Maybe String -> [MDB_DbFlag] -> IO MDB_dbi'
|
|
mdb_dbi_open' txn dbName flags = MDB_dbi' <$> mdb_dbi_open_t txn dbName flags
|
|
|
|
mdb_stat' :: MDB_txn -> MDB_dbi' -> IO MDB_stat
|
|
mdb_stat' txn = mdb_stat_t txn . _dbi'
|
|
|
|
mdb_dbi_flags' :: MDB_txn -> MDB_dbi' -> IO [MDB_DbFlag]
|
|
mdb_dbi_flags' txn = mdb_dbi_flags_t txn . _dbi'
|
|
|
|
mdb_dbi_close' :: MDB_env -> MDB_dbi' -> IO ()
|
|
mdb_dbi_close' txn = mdb_dbi_close_t txn . _dbi'
|
|
|
|
mdb_drop' :: MDB_txn -> MDB_dbi' -> IO ()
|
|
mdb_drop' txn = mdb_drop_t txn . _dbi'
|
|
|
|
mdb_clear' :: MDB_txn -> MDB_dbi' -> IO ()
|
|
mdb_clear' txn = mdb_clear_t txn . _dbi'
|
|
|
|
-- | use a nullable CString
|
|
withCStringMaybe :: Maybe String -> (CString -> IO a) -> IO a
|
|
withCStringMaybe Nothing f = f nullPtr
|
|
withCStringMaybe (Just s) f = withCString s f
|
|
|
|
mdb_dbi_open_t :: MDB_txn -> Maybe String -> [MDB_DbFlag] -> IO MDB_dbi_t
|
|
mdb_dbi_open_t txn dbName flags = -- use string name
|
|
let cdbFlags = compileDBFlags flags in
|
|
alloca $ \ pDBI ->
|
|
withCStringMaybe dbName $ \ cdbName ->
|
|
_mdb_dbi_open (_txn_ptr txn) cdbName cdbFlags pDBI >>= \ rc ->
|
|
if (0 == rc) then peek pDBI else
|
|
_throwLMDBErrNum "mdb_dbi_open" rc
|
|
|
|
mdb_stat_t :: MDB_txn -> MDB_dbi_t -> IO MDB_stat
|
|
mdb_stat_t txn dbi =
|
|
alloca $ \ pStat ->
|
|
_mdb_stat (_txn_ptr txn) dbi pStat >>= \ rc ->
|
|
if (0 == rc) then peek pStat else
|
|
_throwLMDBErrNum "mdb_stat" rc
|
|
|
|
mdb_dbi_flags_t :: MDB_txn -> MDB_dbi_t -> IO [MDB_DbFlag]
|
|
mdb_dbi_flags_t txn dbi =
|
|
alloca $ \ pFlags ->
|
|
_mdb_dbi_flags (_txn_ptr txn) dbi pFlags >>= \ rc ->
|
|
if (0 == rc) then decompileDBFlags <$> peek pFlags else
|
|
_throwLMDBErrNum "mdb_dbi_flags" rc
|
|
|
|
mdb_dbi_close_t :: MDB_env -> MDB_dbi_t -> IO ()
|
|
mdb_dbi_close_t env dbi = _mdb_dbi_close (_env_ptr env) dbi
|
|
|
|
mdb_drop_t :: MDB_txn -> MDB_dbi_t -> IO ()
|
|
mdb_drop_t txn dbi =
|
|
_mdb_drop (_txn_ptr txn) dbi 1 >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_drop" rc)
|
|
|
|
mdb_clear_t :: MDB_txn -> MDB_dbi_t -> IO ()
|
|
mdb_clear_t txn dbi =
|
|
_mdb_drop (_txn_ptr txn) dbi 0 >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_clear" rc)
|
|
|
|
-- | Set a user-defined key comparison function for a database.
|
|
mdb_set_compare :: MDB_txn -> MDB_dbi -> FunPtr MDB_cmp_func -> IO ()
|
|
mdb_set_compare txn dbi fcmp =
|
|
_mdb_set_compare (_txn_ptr txn) dbi fcmp >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_set_compare" rc)
|
|
|
|
-- | Set a user-defined data comparison operator for MDB_DUPSORT databases.
|
|
mdb_set_dupsort :: MDB_txn -> MDB_dbi -> FunPtr MDB_cmp_func -> IO ()
|
|
mdb_set_dupsort txn dbi fcmp =
|
|
_mdb_set_dupsort (_txn_ptr txn) dbi fcmp >>= \ rc ->
|
|
unless (0 == rc) (_throwLMDBErrNum "mdb_set_dupsort" rc)
|
|
|
|
-- zero datum
|
|
zed :: MDB_val
|
|
zed = MDB_val 0 nullPtr
|
|
|
|
-- | Access a value by key. Returns Nothing if the key is not found.
|
|
mdb_get :: MDB_txn -> MDB_dbi -> MDB_val -> IO (Maybe MDB_val)
|
|
mdb_get txn dbi key =
|
|
withKVPtrs key zed $ \ pKey pVal ->
|
|
_mdb_get (_txn_ptr txn) dbi pKey pVal >>= \ rc ->
|
|
r_get rc pVal
|
|
{-# INLINE mdb_get #-}
|
|
|
|
r_get :: CInt -> Ptr MDB_val -> IO (Maybe MDB_val)
|
|
r_get rc pVal =
|
|
if (0 == rc) then Just <$> peek pVal else
|
|
if ((#const MDB_NOTFOUND) == rc) then return Nothing else
|
|
_throwLMDBErrNum "mdb_get" rc
|
|
{-# INLINE r_get #-}
|
|
|
|
-- | utility function: prepare pointers suitable for mdb_cursor_get.
|
|
withKVPtrs :: MDB_val -> MDB_val -> (Ptr MDB_val -> Ptr MDB_val -> IO a) -> IO a
|
|
withKVPtrs k v fn =
|
|
allocaBytes (2 * sizeOf k) $ \ pK ->
|
|
let pV = pK `plusPtr` sizeOf k in
|
|
do poke pK k
|
|
poke pV v
|
|
fn pK pV
|
|
{-# INLINE withKVPtrs #-}
|
|
|
|
-- | variation on withKVPtrs with nullable value.
|
|
withKVOptPtrs :: MDB_val -> Maybe MDB_val -> (Ptr MDB_val -> Ptr MDB_val -> IO a) -> IO a
|
|
withKVOptPtrs k (Just v) fn = withKVPtrs k v fn
|
|
withKVOptPtrs k Nothing fn = alloca $ \ pK -> poke pK k >> fn pK nullPtr
|
|
|
|
|
|
-- | Add a (key,value) pair to the database.
|
|
--
|
|
-- Returns False on MDB_KEYEXIST, and True on MDB_SUCCESS. Any other
|
|
-- return value from LMDB results in an exception. The MDB_KEYEXIST
|
|
-- result can be returned only if certain write flags are enabled.
|
|
mdb_put :: MDB_WriteFlags -> MDB_txn -> MDB_dbi -> MDB_val -> MDB_val -> IO Bool
|
|
mdb_put wf txn dbi key val =
|
|
withKVPtrs key val $ \ pKey pVal ->
|
|
_mdb_put (_txn_ptr txn) dbi pKey pVal wf >>= \ rc ->
|
|
r_put rc
|
|
{-# INLINE mdb_put #-}
|
|
|
|
r_put :: CInt -> IO Bool
|
|
r_put rc =
|
|
if (0 == rc) then return True else
|
|
if ((#const MDB_KEYEXIST) == rc) then return False else
|
|
_throwLMDBErrNum "mdb_put" rc
|
|
{-# INLINE r_put #-}
|
|
|
|
-- | Allocate space for data under a given key. This space must be
|
|
-- filled before the write transaction commits. The idea here is to
|
|
-- avoid an extra allocation.
|
|
--
|
|
-- mdb_reserve flags txn dbi key byteCount
|
|
--
|
|
-- Note: not safe to use with MDB_DUPSORT.
|
|
-- Note: MDB_KEYEXIST will result in an exception here.
|
|
mdb_reserve :: MDB_WriteFlags -> MDB_txn -> MDB_dbi -> MDB_val -> Int -> IO MDB_val
|
|
mdb_reserve wf txn dbi key szBytes =
|
|
withKVPtrs key (reserveData szBytes) $ \ pKey pVal ->
|
|
_mdb_put (_txn_ptr txn) dbi pKey pVal (wfReserve wf) >>= \ rc ->
|
|
if (0 == rc) then peek pVal else
|
|
_throwLMDBErrNum "mdb_reserve" rc
|
|
{-# INLINE mdb_reserve #-}
|
|
|
|
wfReserve :: MDB_WriteFlags -> MDB_WriteFlags
|
|
wfReserve (MDB_WriteFlags wf) = MDB_WriteFlags ((#const MDB_RESERVE) .|. wf)
|
|
|
|
reserveData :: Int -> MDB_val
|
|
reserveData szBytes = MDB_val (fromIntegral szBytes) nullPtr
|
|
|
|
-- | Delete a given key, or a specific (key,value) pair in case of
|
|
-- MDB_DUPSORT. This function will return False on a MDB_NOTFOUND
|
|
-- result, and True on MDB_SUCCESS.
|
|
--
|
|
-- Note: Ideally, LMDB would match the value even without MDB_DUPSORT.
|
|
-- But it doesn't. Under the hood, the data is replaced by a null ptr
|
|
-- if MDB_DUPSORT is not enabled (v0.9.10).
|
|
mdb_del :: MDB_txn -> MDB_dbi -> MDB_val -> Maybe MDB_val -> IO Bool
|
|
mdb_del txn dbi key mbVal =
|
|
withKVOptPtrs key mbVal $ \ pKey pVal ->
|
|
_mdb_del (_txn_ptr txn) dbi pKey pVal >>= \ rc ->
|
|
r_del rc
|
|
{-# INLINE mdb_del #-}
|
|
|
|
r_del :: CInt -> IO Bool
|
|
r_del rc =
|
|
if (0 == rc) then return True else
|
|
if ((#const MDB_NOTFOUND) == rc) then return False else
|
|
_throwLMDBErrNum "mdb_del" rc
|
|
{-# INLINE r_del #-}
|
|
|
|
mdb_get' :: MDB_txn -> MDB_dbi' -> MDB_val -> IO (Maybe MDB_val)
|
|
mdb_get' txn dbi key =
|
|
withKVPtrs key zed $ \ pKey pVal ->
|
|
_mdb_get' (_txn_ptr txn) dbi pKey pVal >>= \ rc ->
|
|
r_get rc pVal
|
|
{-# INLINE mdb_get' #-}
|
|
|
|
mdb_put' :: MDB_WriteFlags -> MDB_txn -> MDB_dbi' -> MDB_val -> MDB_val -> IO Bool
|
|
mdb_put' wf txn dbi key val =
|
|
withKVPtrs key val $ \ pKey pVal ->
|
|
_mdb_put' (_txn_ptr txn) dbi pKey pVal wf >>= \ rc ->
|
|
r_put rc
|
|
{-# INLINE mdb_put' #-}
|
|
|
|
mdb_reserve' :: MDB_WriteFlags -> MDB_txn -> MDB_dbi' -> MDB_val -> Int -> IO MDB_val
|
|
mdb_reserve' wf txn dbi key szBytes =
|
|
withKVPtrs key (reserveData szBytes) $ \ pKey pVal ->
|
|
_mdb_put' (_txn_ptr txn) dbi pKey pVal (wfReserve wf) >>= \ rc ->
|
|
if (0 == rc) then peek pVal else
|
|
_throwLMDBErrNum "mdb_reserve" rc
|
|
{-# INLINE mdb_reserve' #-}
|
|
|
|
mdb_del' :: MDB_txn -> MDB_dbi' -> MDB_val -> Maybe MDB_val -> IO Bool
|
|
mdb_del' txn dbi key mbVal =
|
|
withKVOptPtrs key mbVal $ \ pKey pVal ->
|
|
_mdb_del' (_txn_ptr txn) dbi pKey pVal >>= \rc ->
|
|
r_del rc
|
|
{-# INLINE mdb_del' #-}
|
|
|
|
-- | compare two values as keys in a database
|
|
mdb_cmp :: MDB_txn -> MDB_dbi -> MDB_val -> MDB_val -> IO Ordering
|
|
mdb_cmp txn dbi a b =
|
|
withKVPtrs a b $ \ pA pB ->
|
|
_mdb_cmp (_txn_ptr txn) dbi pA pB >>= \ rc ->
|
|
return (compare rc 0)
|
|
{-# INLINE mdb_cmp #-}
|
|
|
|
-- | compare two values as data in an MDB_DUPSORT database
|
|
mdb_dcmp :: MDB_txn -> MDB_dbi -> MDB_val -> MDB_val -> IO Ordering
|
|
mdb_dcmp txn dbi a b =
|
|
withKVPtrs a b $ \ pA pB ->
|
|
_mdb_dcmp (_txn_ptr txn) dbi pA pB >>= \ rc ->
|
|
return (compare rc 0)
|
|
{-# INLINE mdb_dcmp #-}
|
|
|
|
mdb_cmp' :: MDB_txn -> MDB_dbi' -> MDB_val -> MDB_val -> IO Ordering
|
|
mdb_cmp' txn dbi a b =
|
|
withKVPtrs a b $ \ pA pB ->
|
|
_mdb_cmp' (_txn_ptr txn) dbi pA pB >>= \ rc ->
|
|
return (compare rc 0)
|
|
{-# INLINE mdb_cmp' #-}
|
|
|
|
mdb_dcmp' :: MDB_txn -> MDB_dbi' -> MDB_val -> MDB_val -> IO Ordering
|
|
mdb_dcmp' txn dbi a b =
|
|
withKVPtrs a b $ \ pA pB ->
|
|
_mdb_dcmp' (_txn_ptr txn) dbi pA pB >>= \ rc ->
|
|
return (compare rc 0)
|
|
{-# INLINE mdb_dcmp' #-}
|
|
|
|
-- | open a cursor for the database.
|
|
mdb_cursor_open :: MDB_txn -> MDB_dbi -> IO MDB_cursor
|
|
mdb_cursor_open txn dbi =
|
|
alloca $ \ ppCursor ->
|
|
_mdb_cursor_open (_txn_ptr txn) dbi ppCursor >>= \ rc ->
|
|
if (0 /= rc) then _throwLMDBErrNum "mdb_cursor_open" rc else
|
|
peek ppCursor >>= \ pCursor ->
|
|
return $! MDB_cursor
|
|
{ _crs_ptr = pCursor
|
|
, _crs_dbi = dbi
|
|
, _crs_txn = txn
|
|
}
|
|
|
|
mdb_cursor_open' :: MDB_txn -> MDB_dbi' -> IO MDB_cursor'
|
|
mdb_cursor_open' txn dbi =
|
|
alloca $ \ ppCursor ->
|
|
_mdb_cursor_open' (_txn_ptr txn) dbi ppCursor >>= \ rc ->
|
|
if (0 /= rc) then _throwLMDBErrNum "mdb_cursor_open" rc else
|
|
peek ppCursor >>= \ pCursor ->
|
|
return $! MDB_cursor'
|
|
{ _crs_ptr' = pCursor
|
|
, _crs_dbi' = dbi
|
|
, _crs_txn' = txn
|
|
}
|
|
|
|
-- | Low-level mdb_cursor_get operation, with direct control of how
|
|
-- pointers to values are allocated, whether an argument is a nullPtr,
|
|
-- and so on.
|
|
--
|
|
-- In this case, False is returned for MDB_NOTFOUND (in which case the
|
|
-- cursor should not be moved), and True is returned for MDB_SUCCESS.
|
|
-- Any other return value from LMDB will result in an exception.
|
|
--
|
|
-- Depending on the MDB_cursor_op, additional values may be returned
|
|
-- via the pointers. At the moment
|
|
mdb_cursor_get :: MDB_cursor_op -> MDB_cursor -> Ptr MDB_val -> Ptr MDB_val -> IO Bool
|
|
mdb_cursor_get op crs pKey pData = _mdb_cursor_get (_crs_ptr crs) pKey pData (cursorOp op) >>= r_cursor_get
|
|
{-# INLINE mdb_cursor_get #-}
|
|
|
|
r_cursor_get :: CInt -> IO Bool
|
|
r_cursor_get rc =
|
|
if(0 == rc) then return True else
|
|
if((#const MDB_NOTFOUND) == rc) then return False else
|
|
_throwLMDBErrNum "mdb_cursor_get" rc
|
|
{-# INLINE r_cursor_get #-}
|
|
|
|
mdb_cursor_get' :: MDB_cursor_op -> MDB_cursor' -> Ptr MDB_val -> Ptr MDB_val -> IO Bool
|
|
mdb_cursor_get' op crs pKey pData = _mdb_cursor_get' (_crs_ptr' crs) pKey pData (cursorOp op) >>= r_cursor_get
|
|
{-# INLINE mdb_cursor_get' #-}
|
|
|
|
-- | Low-level 'mdb_cursor_put' operation.
|
|
--
|
|
-- As with mdb_put, this returns True on MDB_SUCCESS and False for MDB_KEYEXIST,
|
|
-- and otherwise throws an exception.
|
|
mdb_cursor_put :: MDB_WriteFlags -> MDB_cursor -> MDB_val -> MDB_val -> IO Bool
|
|
mdb_cursor_put wf crs key val =
|
|
withKVPtrs key val $ \ pKey pVal ->
|
|
_mdb_cursor_put (_crs_ptr crs) pKey pVal wf >>= \ rc ->
|
|
r_cursor_put rc
|
|
{-# INLINE mdb_cursor_put #-}
|
|
|
|
r_cursor_put :: CInt -> IO Bool
|
|
r_cursor_put rc =
|
|
if(0 == rc) then return True else
|
|
if((#const MDB_KEYEXIST) == rc) then return False else
|
|
_throwLMDBErrNum "mdb_cursor_put" rc
|
|
{-# INLINE r_cursor_put #-}
|
|
|
|
mdb_cursor_put' :: MDB_WriteFlags -> MDB_cursor' -> MDB_val -> MDB_val -> IO Bool
|
|
mdb_cursor_put' wf crs key val =
|
|
withKVPtrs key val $ \ pKey pVal ->
|
|
_mdb_cursor_put' (_crs_ptr' crs) pKey pVal wf >>= \ rc ->
|
|
r_cursor_put rc
|
|
{-# INLINE mdb_cursor_put' #-}
|
|
|
|
-- | Delete the value at the cursor.
|
|
mdb_cursor_del :: MDB_WriteFlags -> MDB_cursor -> IO ()
|
|
mdb_cursor_del wf crs = _mdb_cursor_del (_crs_ptr crs) wf >>= r_cursor_del
|
|
{-# INLINE mdb_cursor_del #-}
|
|
|
|
r_cursor_del :: CInt -> IO ()
|
|
r_cursor_del rc = unless (0 == rc) (_throwLMDBErrNum "mdb_cursor_del" rc)
|
|
{-# INLINE r_cursor_del #-}
|
|
|
|
mdb_cursor_del' :: MDB_WriteFlags -> MDB_cursor' -> IO ()
|
|
mdb_cursor_del' wf crs = _mdb_cursor_del' (_crs_ptr' crs) wf >>= r_cursor_del
|
|
{-# INLINE mdb_cursor_del' #-}
|
|
|
|
-- | Close a cursor. don't use after this. In general, cursors should
|
|
-- be closed before their associated transaction is commited or aborted.
|
|
mdb_cursor_close :: MDB_cursor -> IO ()
|
|
mdb_cursor_close crs = _mdb_cursor_close (_crs_ptr crs)
|
|
|
|
mdb_cursor_close' :: MDB_cursor' -> IO ()
|
|
mdb_cursor_close' crs = _mdb_cursor_close' (_crs_ptr' crs)
|
|
|
|
-- | Access transaction associated with a cursor.
|
|
mdb_cursor_txn :: MDB_cursor -> MDB_txn
|
|
mdb_cursor_txn = _crs_txn
|
|
|
|
mdb_cursor_txn' :: MDB_cursor' -> MDB_txn
|
|
mdb_cursor_txn' = _crs_txn'
|
|
|
|
-- | Access the database associated with a cursor.
|
|
mdb_cursor_dbi :: MDB_cursor -> MDB_dbi
|
|
mdb_cursor_dbi = _crs_dbi
|
|
|
|
mdb_cursor_dbi' :: MDB_cursor' -> MDB_dbi'
|
|
mdb_cursor_dbi' = _crs_dbi'
|
|
|
|
-- | count number of duplicate data items at cursor's current location.
|
|
mdb_cursor_count :: MDB_cursor -> IO Int
|
|
mdb_cursor_count crs =
|
|
alloca $ \ pCount ->
|
|
_mdb_cursor_count (_crs_ptr crs) pCount >>= \ rc ->
|
|
if (0 == rc) then fromIntegral <$> _peekSize pCount else
|
|
_throwLMDBErrNum "mdb_cursor_count" rc
|
|
{-# INLINE mdb_cursor_count #-}
|
|
|
|
_peekSize :: Ptr CSize -> IO CSize
|
|
_peekSize = peek
|
|
|
|
mdb_cursor_count' :: MDB_cursor' -> IO Int
|
|
mdb_cursor_count' crs =
|
|
alloca $ \ pCount ->
|
|
_mdb_cursor_count' (_crs_ptr' crs) pCount >>= \ rc ->
|
|
if (0 == rc) then fromIntegral <$> _peekSize pCount else
|
|
_throwLMDBErrNum "mdb_cursor_count" rc
|
|
{-# INLINE mdb_cursor_count' #-}
|
|
|
|
|
|
-- for cursor get...
|
|
-- I'm not really sure what I want to do here, not quite yet.
|
|
-- maybe I should write a bunch of individual functions?
|
|
|
|
{-
|
|
foreign import ccall safe "lmdb.h mdb_cursor_get" _mdb_cursor_get :: Ptr MDB_cursor -> Ptr MDB_val -> Ptr MDB_val -> (#type MDB_cursor_op) -> IO CInt
|
|
-}
|
|
|
|
{-
|
|
cmpBytesToCmpFn :: (ByteString -> ByteString -> Ord) -> CmpFn
|
|
cmpBytesToCmpFn cmp vL vR = do
|
|
lBytes <- valToVolatileByteString vL
|
|
rBytes <- valToVolatileByteString vR
|
|
return $! case cmp lBytes rBytes of
|
|
LT -> -1
|
|
EQ -> 0
|
|
GT -> 1
|
|
|
|
-- | Create a user-defined comparison funcion over ByteStrings
|
|
wrapCmpBytes :: (ByteString -> ByteString -> Ord) -> MDB_cmp_func
|
|
wrapCmpBytes = as_MDB_cmp_func . cmpBytesToCmpFn
|
|
|
|
-- | Convert a value to a bytestring in O(1) time. Note, however,
|
|
-- that this bytestring refers into a memory-mapped page in the
|
|
-- database, which may be reused after the transaction that obtained
|
|
-- the value is dropped. Developers must be careful to ensure the
|
|
-- bytestring doesn't stick around in any lazy computations.
|
|
--
|
|
-- Consider use of the safer, higher level API that will strongly
|
|
-- associate a value with a particular transaction.
|
|
valToBytes :: MDB_val -> IO ByteString
|
|
valToBytes (MDB_val sz pd) = do
|
|
fpd <- newForeignPtr_ pd
|
|
return $! B.fromForeignPtr fpd 0 (fromIntegral sz)
|
|
|
|
-}
|
|
|
|
|
|
instance Storable MDB_val where
|
|
alignment _ = #{alignment MDB_val}
|
|
sizeOf _ = #{size MDB_val}
|
|
peek ptr = do
|
|
sz <- #{peek MDB_val, mv_size} ptr
|
|
pd <- #{peek MDB_val, mv_data} ptr
|
|
return $! MDB_val sz pd
|
|
poke ptr (MDB_val sz pd) = do
|
|
#{poke MDB_val, mv_size} ptr sz
|
|
#{poke MDB_val, mv_data} ptr pd
|
|
|
|
instance Storable MDB_stat where
|
|
alignment _ = #{alignment MDB_stat}
|
|
sizeOf _ = #{size MDB_stat}
|
|
peek ptr = do
|
|
psize <- #{peek MDB_stat, ms_psize} ptr
|
|
depth <- #{peek MDB_stat, ms_depth} ptr
|
|
branch_pages <- #{peek MDB_stat, ms_branch_pages} ptr
|
|
leaf_pages <- #{peek MDB_stat, ms_leaf_pages} ptr
|
|
overflow_pages <- #{peek MDB_stat, ms_overflow_pages} ptr
|
|
entries <- #{peek MDB_stat, ms_entries} ptr
|
|
return $! MDB_stat
|
|
{ ms_psize = psize
|
|
, ms_depth = depth
|
|
, ms_branch_pages = branch_pages
|
|
, ms_leaf_pages = leaf_pages
|
|
, ms_overflow_pages = overflow_pages
|
|
, ms_entries = entries
|
|
}
|
|
poke ptr val = do
|
|
#{poke MDB_stat, ms_psize} ptr (ms_psize val)
|
|
#{poke MDB_stat, ms_depth} ptr (ms_depth val)
|
|
#{poke MDB_stat, ms_branch_pages} ptr (ms_branch_pages val)
|
|
#{poke MDB_stat, ms_leaf_pages} ptr (ms_leaf_pages val)
|
|
#{poke MDB_stat, ms_overflow_pages} ptr (ms_overflow_pages val)
|
|
#{poke MDB_stat, ms_entries} ptr (ms_entries val)
|
|
|
|
instance Storable MDB_envinfo where
|
|
alignment _ = #{alignment MDB_envinfo}
|
|
sizeOf _ = #{size MDB_envinfo}
|
|
peek ptr = do
|
|
mapaddr <- #{peek MDB_envinfo, me_mapaddr} ptr
|
|
mapsize <- #{peek MDB_envinfo, me_mapsize} ptr
|
|
last_pgno <- #{peek MDB_envinfo, me_last_pgno} ptr
|
|
last_txnid <- #{peek MDB_envinfo, me_last_txnid} ptr
|
|
maxreaders <- #{peek MDB_envinfo, me_maxreaders} ptr
|
|
numreaders <- #{peek MDB_envinfo, me_numreaders} ptr
|
|
return $! MDB_envinfo
|
|
{ me_mapaddr = mapaddr
|
|
, me_mapsize = mapsize
|
|
, me_last_pgno = last_pgno
|
|
, me_last_txnid = MDB_txnid last_txnid
|
|
, me_maxreaders = maxreaders
|
|
, me_numreaders = numreaders
|
|
}
|
|
poke ptr val = do
|
|
#{poke MDB_envinfo, me_mapaddr} ptr (me_mapaddr val)
|
|
#{poke MDB_envinfo, me_mapsize} ptr (me_mapsize val)
|
|
#{poke MDB_envinfo, me_last_pgno} ptr (me_last_pgno val)
|
|
#{poke MDB_envinfo, me_last_txnid} ptr (_txnid $ me_last_txnid val)
|
|
#{poke MDB_envinfo, me_maxreaders} ptr (me_maxreaders val)
|
|
#{poke MDB_envinfo, me_numreaders} ptr (me_numreaders val)
|
|
|
|
|