packages feed

libmdbx-0.2.1.1: src/Mdbx/Types.hs

{-|
Module      : Mdbx.Types
Copyright   : (c) 2021 Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Types used by the library. Mainly re exports the types generated by c2hs in the
FFI module, while it also adds some types used by the high level interface.
-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Mdbx.Types (
  -- * Re-exported from FFI
  MdbxEnv,
  MdbxTxn,
  MdbxDbi,
  MdbxCursor,
  MdbxVal(..),
  MdbxEnvMode(..),
  MdbxEnvFlags(..),
  MdbxTxnFlags(..),
  MdbxDbFlags(..),
  MdbxPutFlags(..),
  MdbxCursorOp(..),
  -- * High level interface
  MdbxEnvGeometry(..),
  MdbxItem(..),
  -- * Helper types
  NullByteString(..),
  NullText(..)
) where

import Data.ByteString (ByteString, packCStringLen, useAsCStringLen)
import Data.ByteString.Short (ShortByteString)
import Data.Default
import Data.String (IsString(..))
import Data.Text (Text)
import Data.Text.Foreign (fromPtr, useAsPtr)
import Foreign.Ptr (castPtr)

import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Short as BSH
import qualified Data.Text as T

import Mdbx.FFI

{-|
Geometry of the database. The most important parameter is the maximum size, that
defaults to 1024Mb. All other values default to -1, meaning the current value
will be kept.
-}
data MdbxEnvGeometry = MdbxEnvGeometry {
  -- | Minimum DB size in bytes.
  envSizeMin :: Int,
  -- | Current DB size in bytes.
  envSizeNow :: Int,
  -- | Maximum DB size in bytes.
  envSizeMax :: Int,
  {-|
  Step growth size of the database in bytes. Must be greater than zero to allow
  for growth.
  -}
  envGrowthStep :: Int,
  {-|
  Step shrink size of the database in bytes. Must be greater than zero to allow
  for shrinkage and lower than envGrowthStep to avoid immediate shrinking after
  growth.
  -}
  envShrinkThreshold :: Int,
  {-|
  Page size of the database in bytes. In general it should not be changed after
  the database was created.
  -}
  envPageSize :: Int
} deriving (Eq, Show)

instance Default MdbxEnvGeometry where
  def = MdbxEnvGeometry {
    envSizeMin = -1,
    envSizeNow = -1,
    envSizeMax = 1024 * 1024 * 1024,
    envGrowthStep = -1,
    envShrinkThreshold = -1,
    envPageSize = -1
  }

{-|
Converts an instance to/from the representation needed by libmdbx. This type is
used for both keys and values. The fields on the type must be strict, otherwise
unexpected crashes due to lazy IO delaying low level memory access may happen.

Only 'ByteString', 'Text' instances are provided, since they are commonly used
as the key when storing/retrieving a value.

For your own types, in general, you will want to use a serialization library
such as <https://hackage.haskell.org/package/binary binary>, and apply the
newtype deriving via trick.

'Mdbx.Binary.MdbxItemBinary' is provided to simplify using 'Data.Store.Binary'
instances as keys or values with libmdbx, while 'Mdbx.Store.MdbxItemStore'
provides the same functionality for 'Data.Store.Store' instances. With those
helpers, creating custom types compatible with libmdbx is easy:

@
data User = User {
  _username :: !Text,
  _password :: !Text
} deriving (Eq, Show, Generic, Store)

deriving via (MdbxItemBinary User) instance MdbxItem User
@

__Note 1:__ if you plan on using a custom type as the key, be careful if it
contains 'Text' or 'ByteString' instances, since these types have a length field
which is serialized before the data. This causes issues when using libmdbx,
since it depends on key ordering and the length field will make shorter
instances lower than longer ones, even if the content indicates the opposite.
You can use the provided 'NullByteString' or 'NullText' types if your data type
is an instance of 'Data.Binary.Binary' or 'Data.Store.Store'. Otherwise, it is
simpler to use 'Text' or 'ByteString' as the key.

__Note 2:__ If your key type contains Word16 or longer fields, you should make
it an instance of 'Data.Binary.Binary', not 'Data.Store.Store', since Store uses
platform dependent endianess and this affects libmdbx's comparison functions.
Given Binary uses network order (big endian) for encoding, the comparison
functions will work as expected. Failing to do this may cause unexpected issues
when retrieving data, in particular when using cursors.

__Note 3:__ The behavior when using signed integers or floating point numbers as
part of the key is undefined. To be able to use these types in the key, you
should store them as a Word of the appropriate size and convert them with the
conversion functions included in `Module.API`.
-}
class MdbxItem i where
  {-|
  Converts a block of memory provided by libmdbx to a user data type. There are
  no guarantees provided by the library that the block of memory matches the
  expected type; a crash can happen when trying to deserialize an incorrect
  type.
  -}
  fromMdbxVal :: MdbxVal -> IO i
  {-|
  Converts a user data type to a block of memory.
  -}
  toMdbxVal :: i -> (MdbxVal -> IO b) -> IO b

instance MdbxItem Text where
  fromMdbxVal (MdbxVal sz ptr) =
    fromPtr (castPtr ptr) (fromIntegral sz `div` 2)

  toMdbxVal val fn = useAsPtr val $ \ptr size ->
    fn $ MdbxVal (fromIntegral size * 2) (castPtr ptr)

instance MdbxItem ByteString where
  fromMdbxVal (MdbxVal sz ptr) =
    packCStringLen (castPtr ptr, fromIntegral sz)

  toMdbxVal val fn = useAsCStringLen val $ \(ptr, size) ->
    fn $ MdbxVal (fromIntegral size) (castPtr ptr)

{-|
Newtype wrapping a 'ByteString' that provides a 'Data.Binary.Binary' instance
using NULL terminated C strings, which allows for using them as part of a custom
data type representing a key.

This is not possible with regular 'ByteString' and 'Text' instances since their
'Data.Binary.Binary' instances are serialized with the size field first. Given
that libmdbx compares keys as an unstructured sequence of bytes, this can cause
issues since longer strings are considered greater than shorter ones, even if
their content indicates otherwise.
-}
newtype NullByteString = NullByteString {
  unNullByteString :: ShortByteString
} deriving newtype (Eq, Ord)

instance Show NullByteString where
  show (NullByteString nbs) = BC.unpack (BSH.fromShort nbs)

instance IsString NullByteString where
  fromString = NullByteString . BSH.toShort . BC.pack

{-|
Newtype wrapping a 'Text' that provides a 'Data.Binary.Binary' instance using
NULL terminated C strings, which allows for using them as part of a custom data
type representing a key.

Check 'NullByteString' for the rationale.
-}
newtype NullText = NullText {
  unNullText :: Text
} deriving newtype (Eq, Ord)

instance Show NullText where
  show (NullText nts) = T.unpack nts

instance IsString NullText where
  fromString = NullText . T.pack