rocksdb-haskell-jprupp
Haskell bindings for RocksDB
https://github.com/jprupp/rocksdb-haskell#readme
| LTS Haskell 24.66: | 2.1.7 |
| Stackage Nightly 2026-11-10: | 2.6.4 |
| Latest on Hackage: | 2.6.4 |
rocksdb-haskell-jprupp-2.6.4@sha256:71fb696753e5ce9743643c4314b7d3bca7e82d0c04e1621280772db9a05e9e67,1606Module documentation for 2.6.4
- Database
rocksdb-haskell-jprupp
Haskell bindings to RocksDB, the embedded persistent
key-value store. The package on Hackage is rocksdb-haskell-jprupp; the module
to import is Database.RocksDB.
The bindings follow the RocksDB C API closely. On top of it they add bracket-style resource management, so that databases, snapshots and iterators are always released, even when an exception is thrown.
Contents
- Why these bindings
- Design principles
- Features
- Installation
- Quick start
- Tutorial: an event log
- Concepts
- Usage guide
- Configuration
- Module overview
- Things to watch out for
- Development
- Related projects
- License
Why these bindings
RocksDB is a storage engine: a library that keeps sorted keys and values in files on local disk, with fast writes, fast point lookups and fast ordered scans. There is no server to run. It is what you reach for when SQLite’s relational model is more than you need, and an in-memory map is not durable or big enough.
These bindings exist because the Haskoin Bitcoin indexer needed them. It keeps the whole Bitcoin block chain indexed by address, transaction and block, which is hundreds of gigabytes, and serves queries from many threads while new blocks are written. That use shaped the library:
- it must be safe to share one database between many threads;
- resources must never leak, because the process runs for a long time;
- memory outside the Haskell heap must be boundable, because RocksDB’s defaults let it grow with the size of the database;
- the hot paths, such as reading many keys at once, must cost as little as the C API allows.
The library is a fork of leveldb-haskell and of earlier RocksDB bindings,
maintained as rocksdb-haskell-jprupp.
Design principles
- Stay close to the C API. Functions map nearly one to one to RocksDB’s C functions, so RocksDB’s own documentation and tuning guides apply directly. Conveniences such as typed keys live in separate libraries (rocksdb-query).
- Scope every resource. Databases, snapshots and iterators are C objects
that must be freed once and never touched again.
withDB,withSnapshotandwithIterfree them when their block ends, even on an exception. - Plain
IO. No monad transformer constraints. Lift into your own monad withliftIO. - Bytes in, bytes out. Keys and values are strict
ByteStrings, which is what RocksDB stores. How you serialize them is your choice. - Don’t crash in C. RocksDB crashes the process if you misuse an iterator.
The Haskell side checks first and turns those cases into no-ops or
Nothing. - Expose what bounds memory. The few settings that turn RocksDB’s memory
use into a fixed budget are first-class fields of
Config.
Features
| Area | What you get |
|---|---|
| Basic access | get, put, delete on strict ByteString keys and values |
| Batches | Atomic write of many puts and deletes, across column families |
| Multi-get | multiGet reads many keys in one call to RocksDB’s MultiGet |
| Column families | Open a database with several independent key spaces; add new ones on open |
| Snapshots | Consistent point-in-time reads across many queries and iterators |
| Iterators | Ordered scans, forwards and backwards, optionally pinned to a snapshot |
| Tuning | Bloom filters, fixed-length prefix extractor, shared LRU block cache |
| Limits | Memtable and WAL size limits, open file limit |
| Administration | Properties (memory use, statistics, SST files), approximate sizes, destroy, repair |
What is not included
- No transactions (
TransactionDB,OptimisticTransactionDB), merge operators, TTL databases, backups or checkpoints. - No manual compaction,
DeleteRangeorSingleDelete. - No custom comparators: keys always sort by their bytes.
Pull requests that add bindings for these are welcome.
Installation
System library
You need the RocksDB C library and its headers.
# Debian and Ubuntu
sudo apt-get install librocksdb-dev
# Fedora
sudo dnf install rocksdb-devel
# macOS with Homebrew
brew install rocksdb
# Nix
nix-shell -p rocksdb
The library links against -lrocksdb. If RocksDB is installed in a
non-standard place, pass its directories with --extra-include-dirs and
--extra-lib-dirs (Cabal or Stack).
The headers are needed to build, not only the library: the binding of the
Bloom filter policy is compiled against rocksdb/c.h, because RocksDB changed
the type of its argument in 6.24 and the header says which one is installed.
Check the library is found
ls /usr/include/rocksdb/c.h # or $(brew --prefix)/include/rocksdb/c.h
pkg-config --modversion rocksdb # prints the installed version, if registered
Step by step with Stack
-
Install Stack and the RocksDB library (above).
-
Create a project:
stack new rocks-demo simple cd rocks-demo -
Add
rocksdb-haskell-jprupp,bytestringanddata-defaulttobuild-dependsinrocks-demo.cabal. -
Put the quick start program in
src/Main.hsand run it:stack runIt creates a database in
/tmp/exampleand prints what it reads back.
If the build fails with cannot find -lrocksdb or rocksdb/c.h: No such file,
the C library is not where the compiler looks. Install the development package,
or pass --extra-include-dirs and --extra-lib-dirs.
With Cabal
build-depends:
base
, bytestring
, data-default
, rocksdb-haskell-jprupp
data-default gives you def for the default Config.
With Stack
The package is in Stackage nightly snapshots. If your resolver does not have
it, or has an older version, add it to stack.yaml:
extra-deps:
- rocksdb-haskell-jprupp-2.6.4
With stack --nix, add RocksDB to the Nix packages:
nix:
packages:
- rocksdb
Quick start
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Default (def)
import Database.RocksDB
main :: IO ()
main = do
let cfg = def {createIfMissing = True, bloomFilter = True}
withDB "/tmp/example" cfg $ \db -> do
put db "hello" "world"
get db "hello" >>= print -- Just "world"
-- atomic batch
write db [Put "a" "1", Put "b" "2", Del "hello"]
-- many keys at once
multiGet db ["a", "b", "c"] >>= print -- [Just "1",Just "2",Nothing]
-- every entry, in key order
withIter db $ \it -> do
iterFirst it
let loop =
iterEntry it >>= \case
Nothing -> return ()
Just kv -> print kv >> iterNext it >> loop
loop
Tutorial: an event log
This builds a small append-only event log, to show how a RocksDB schema is designed around sorted keys.
1. Design the keys
RocksDB sorts keys by their bytes. Records read together should therefore sort together. Events are read per user and in time order, so the key is the user followed by a big-endian timestamp, which sorts numerically:
{-# LANGUAGE OverloadedStrings #-}
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import Data.Word (Word64)
eventKey :: ByteString -> Word64 -> ByteString
eventKey user time =
BL.toStrict . BB.toLazyByteString $
BB.byteString user <> BB.char7 '/' <> BB.word64BE time
2. Write atomically
An event and a per-user counter must stay consistent, so they go in one batch:
import Database.RocksDB
logEvent :: DB -> ByteString -> Word64 -> ByteString -> Int -> IO ()
logEvent db user time payload count =
write db
[ Put (eventKey user time) payload,
Put ("count/" <> user) (BL.toStrict (BB.toLazyByteString (BB.intDec count)))
]
If the program dies halfway, either both writes are there or neither is.
3. Read a range with an iterator
All events of a user sort together, so one seek and a short scan read them:
eventsOf :: DB -> ByteString -> IO [ByteString]
eventsOf db user = withIter db $ \it -> do
let prefix = user <> "/"
iterSeek it prefix
let go acc = do
mkv <- iterEntry it
case mkv of
Just (k, v) | prefix `B.isPrefixOf` k -> iterNext it >> go (v : acc)
_ -> return (reverse acc)
go []
4. Read consistently with a snapshot
If another thread logs events while you read the counter and then the events, the two can disagree. Read both through a snapshot:
report :: DB -> ByteString -> IO (Maybe ByteString, [ByteString])
report db user = withSnapshot db $ \snap ->
(,) <$> get snap ("count/" <> user) <*> eventsOf snap user
5. Tune for size
Once the log holds millions of events, open it with a memory budget and a Bloom filter for faster point lookups:
let cfg =
def
{ createIfMissing = True,
bloomFilter = True,
maxFiles = Just 1000,
blockCacheSize = Just (512 * 1024 * 1024),
cacheIndexAndFilterBlocks = True
}
Concepts
The database handle
withDB and withDBCF open a database, pass a DB to your function, and close
it when the function returns or throws. The DB must not escape that function.
It is safe to use from many threads at the same time.
Keys and values
Keys and values are strict ByteStrings. RocksDB sorts keys by their bytes,
which is the order iterators walk them in. To store Haskell types, serialize
them yourself, or use rocksdb-query.
Column families
A database can hold several key spaces called column families, each with its
own configuration. Functions ending in CF act on a given column family; the
others act on the default one. The handles are in columnFamilies db, in the
order you listed them when opening.
Snapshots
withSnapshot db f passes f a DB that reads from a frozen view of the
database. Reads and iterators through it do not see later writes. Writes
through it still go to the live database.
Errors
Failures reported by RocksDB are thrown as IOError (built with userError).
The message starts with the operation, such as put: ... or open: ....
Usage guide
Column families
withDBCF "/tmp/example-cf" cfg [("users", cfg), ("orders", cfg)] $ \db -> do
let [users, orders] = columnFamilies db
putCF db users "alice" "..."
getCF db orders "1234" >>= print
write db [PutCF users "bob" "...", DelCF orders "1234"]
On an empty directory, withDBCF creates the database and all the column
families. On an existing database, RocksDB needs every column family that
exists to be listed. To add a new one, list it and set
createMissingColumnFamilies = True in the database Config.
Snapshots
withSnapshot db $ \snap -> do
a <- get snap "a"
b <- get snap "b" -- same point in time as "a"
withIter snap $ \it -> iterFirst it >> iterEntry it >>= print
When the snapshot’s lifetime does not fit a scope, use createSnapshot and
releaseSnapshot. withIterSnap and withIterSnapCF iterate over a raw
Snapshot.
Iterators
An iterator starts out unpositioned. Move it with iterFirst, iterLast or
iterSeek, then step with iterNext and iterPrev. iterKey, iterValue and
iterEntry return Nothing once it moves past either end.
-- every key that starts with "user:"
withIter db $ \it -> do
iterSeek it "user:"
let go acc =
iterEntry it >>= \case
Just (k, v) | "user:" `B.isPrefixOf` k -> iterNext it >> go ((k, v) : acc)
_ -> return (reverse acc)
users <- go []
iterGetError it >>= mapM_ (fail . show) -- tell the end apart from an error
return users
createIterator, createIteratorSnap and destroyIterator manage iterators
by hand.
Properties and maintenance
getProperty db Stats >>= mapM_ BS.putStr
getProperty db BlockCacheUsage
getPropertyCF db cf CurSizeAllMemTables
approximateSize db ("a", "z")
destroy deletes a closed database and repair tries to recover a damaged one.
Both take a raw Options pointer, which the public API does not let you build
yet, so in practice they are only usable from inside the library.
Configuration
Config has a Default instance: all flags off and all sizes left to RocksDB.
| Field | Scope | Meaning |
|---|---|---|
createIfMissing |
database | Create the database if it does not exist |
errorIfExists |
database | Fail if the database already exists |
paranoidChecks |
database | Check data aggressively and stop on corruption |
createMissingColumnFamilies |
database | Create column families given to withDBCF that are missing |
maxFiles |
database | Maximum number of open files (default unlimited) |
dbWriteBufferSize |
database | Bytes of all memtables together (default unlimited) |
blockCacheSize |
database | Bytes of an LRU block cache shared by all column families |
maxTotalWalSize |
database | Bytes of write-ahead log that trigger flushes |
writeBufferSize |
column family | Bytes per memtable (RocksDB default 64 MiB) |
bloomFilter |
column family | Full Bloom filter with 10 bits per key |
prefixLength |
column family | Length of a fixed key prefix for the prefix extractor |
cacheIndexAndFilterBlocks |
column family | Keep index and filter blocks in the block cache |
Database-wide fields are ignored in the configuration of a column family.
Choosing a configuration
RocksDB’s defaults are tuned for small databases. Its memory use outside the Haskell heap comes from three places:
- Memtables, where recent writes collect before being flushed to disk.
Capped per column family by
writeBufferSizeand in total bydbWriteBufferSize. - Index and filter blocks of every open table file. Without
cacheIndexAndFilterBlocksthey live outside any cache and grow with the number of files;maxFileslimits how many are open. - The block cache of recently read data.
blockCacheSizemakes one cache shared by all column families, so memory goes where reads are.
A reasonable starting point for a large database is a block cache of a few
hundred megabytes to a few gigabytes, cacheIndexAndFilterBlocks = True,
maxFiles of a few thousand, and dbWriteBufferSize of a few hundred
megabytes. Enable bloomFilter when most reads are point lookups of keys that
may not exist. A filter takes about 1.25 bytes per key, held with the index
blocks: in the block cache with cacheIndexAndFilterBlocks, in memory outside
it otherwise. A filter belongs to a table file and is written with it, so
turning bloomFilter on for an existing database only covers the files
written from then on; the others get a filter when compaction rewrites them.
For a large database, setting maxFiles, blockCacheSize,
cacheIndexAndFilterBlocks and dbWriteBufferSize puts a ceiling on the memory
RocksDB uses outside the Haskell heap. Without them, index and filter blocks of
every open file and every memtable can grow without bound.
Module overview
Database.RocksDB is the only exposed module. It re-exports:
| Part | Contents |
|---|---|
| Types | DB, Config, BatchOp, ColumnFamily, Snapshot, Iterator, Property, Range |
| Opening | withDB, withDBCF |
| Reading and writing | get, getCF, multiGet, multiGetCF, put, putCF, delete, deleteCF, write |
| Snapshots | withSnapshot, createSnapshot, releaseSnapshot |
| Creating iterators | withIter, withIterCF, withIterSnap, withIterSnapCF |
| Moving iterators | iterSeek, iterFirst, iterLast, iterNext, iterPrev |
| Reading from iterators | iterKey, iterValue, iterEntry, iterValid, iterGetError |
| Administration | getProperty, getPropertyCF, approximateSize, destroy, repair |
The FFI bindings (Database.RocksDB.C) and helpers (Database.RocksDB.Internal)
are hidden modules.
Things to watch out for
- Do not use a
DB, snapshot or iterator after the function that gave it to you has returned. The C object is gone and RocksDB will crash. - Only one process can open a database at a time. A second open fails with a lock error.
- Moving an invalid iterator does nothing. Reposition it with
iterSeek,iterFirstoriterLast. - An iterator that becomes invalid because of corruption looks like one that
reached the end. Check
iterGetError. - Calls into RocksDB are
safeforeign calls, so they do not block other Haskell threads, but each one costs a little more than anunsafecall. - Releases before 2.6.2 built no Bloom filters on RocksDB 6.24 or later,
whatever
bloomFiltersaid: they passed the bits per key as an integer to a function that had come to take adouble. A database written by one of them has no filters in its table files. After upgrading, files get them as they are written, and the memory the filters need comes with them. The size of the filters of a column family is in therocksdb.aggregated-table-propertiesproperty, asfilter block size. - The compiler does not check a foreign import against RocksDB’s header. A binding with the wrong type builds and runs, with garbage in place of the argument; the two fixed in 2.6.2 went unnoticed for years.
Development
Stack
git clone https://github.com/jprupp/rocksdb-haskell.git
cd rocksdb-haskell
stack build # build the library
stack test # run the tests
stack haddock --no-haddock-deps # build the API documentation
With stack --nix, the nix section of stack.yaml provides RocksDB.
Nix
nix develop
cabal build all
cabal test all
The shell includes GHC 9.8.4, cabal, haskell-language-server, fourmolu, hlint and hoogle.
package.yaml is the source of the package description. Change it rather than
rocksdb-haskell-jprupp.cabal, which hpack
generates.
The tests are in test/Spec.hs and use hspec and
QuickCheck. Each test runs against a fresh database in a temporary directory,
so they need RocksDB installed and nothing else. There are two kinds:
- Examples pin down one behaviour each: what a function returns, when it throws, what a setting changes in the files RocksDB writes. A test of a setting checks an effect that can be observed, such as the size of the filter blocks, because a wrong binding can do nothing without failing.
- Properties apply random writes to the database and to a
Map, and require that every way of reading the database agrees with the map: point reads,multiGet, iteration in both directions, seeks, snapshots and column families. The keys are short and drawn from the bytes a binding is most likely to mishandle, zero and 255, so that operations often meet on the same key.
destroy and repair have no tests: they take the Options of the C API,
which this package gives no way to create.
Contributing
Pull requests are welcome.
- Format new code with fourmolu, which the Nix shell provides.
- Add tests for the behaviour you change.
- When you add or change a binding in
Database.RocksDB.C, read the prototype inrocksdb/c.hargument by argument: nothing else checks the Haskell type against it. If the type of an argument differs between versions of RocksDB, import the function withcapi. - Describe the change in
CHANGELOG.md. Versions follow the Haskell Package Versioning Policy.
Related projects
- rocksdb-query stores and queries Haskell types with these bindings.
- haskoin-store is a Bitcoin block chain indexer built on them.
This library started as a fork of leveldb-haskell and of earlier RocksDB bindings. See AUTHORS.
License
rocksdb-haskell-jprupp is released under the BSD 3-Clause License.
Changes
2.6.4
- Iterators read rows about three times faster.
iterValid,iterKeyanditerValuenow reach RocksDB through unsafe foreign calls. The three only read what the iterator already holds, and reading one row withiterEntryanditerNextmakes five of them; as safe calls each cost about 0.4 microseconds. Walking 700,000 rows took 2.8 microseconds a row before and 0.8 after. The calls that move an iterator, which can read from disk, are safe calls as before.
2.6.3
- Documentation only; the library behaves as in 2.6.2.
- References in the Haddock that named both a type and its constructor, such
as
DBandConfig, now say that they mean the type, and two that named nothing in scope are fixed: the documentation builds without warnings about ambiguous or unresolved identifiers. - The documentation of
withItersaid to set a snapshot in the read options, which the API has no way to do. It now says how: create the iterator from theDBthatwithSnapshotprovides. - Remove an unused import.
- The test suite grows from 17 tests to 51. It now covers deletes, batches,
withSnapshot, iteration backwards, empty keys and values, binary data, and the errors of opening a database, none of which had a test. Six of the tests are QuickCheck properties that compare the database with aMapafter random writes. The suite also compiles without warnings. - A badge in the README shows the state of CI on
master.
2.6.2
-
Build the Bloom filters that
bloomFilterasks for. The bits per key were passed torocksdb_filterpolicy_create_bloom_fullas an integer, which is what RocksDB took until 6.23; since 6.24 the function takes adouble. An integer and adoubletravel in different registers, so RocksDB read something else as the bits per key and built no filters: table files hadfilter block size=0, and a lookup of an absent key read a data block in every file that could hold it. Nothing failed, and no test noticed. In a measurement with 2,600,000 keys, such a lookup took about 5.5 microseconds without the filters and 1.5 with them.The function is now imported with the
capicalling convention, through a stub compiled againstrocksdb/c.h, so the C compiler passes the argument as the installed RocksDB declares it, before or after 6.24. The headers were already needed to install RocksDB for development; they are now needed to build this package too.Databases opened with
bloomFilterstart building filters when they are opened with this version: for the table files written from then on, at about 1.25 bytes per key. Files written before have none until compaction rewrites them. -
Pass the error pointer that
rocksdb_approximate_sizestakes since RocksDB 6.16.approximateSizecalled it with one argument too few, leaving RocksDB to read a pointer that was never passed, and to write an error message through it if the call failed. It now throws anIOErroron failure. -
Both were found by comparing every foreign import with the
rocksdb/c.hof RocksDB 11.1: the other 70 match. -
Tests that table files have filters when asked for and not otherwise, and of
approximateSize, which had none.
2.6.1
- Documentation, lots of it, with detailed explanations.
2.6.0
- Add
multiGetandmultiGetCF, which read the values of many keys in one call to RocksDB’sMultiGet. - Add
blockCacheSizetoConfig: the bytes of a least-recently-used block cache shared by the database and all its column families. Without it RocksDB gives each column family its own default cache, of 32 MiB in RocksDB 8. - Add
cacheIndexAndFilterBlockstoConfig, to keep the index and filter blocks of a column family in the block cache, at high priority, with those of level 0 pinned, instead of in memory outside the cache for every open file. - Stop leaking the block-based table options of every database and column family opened with a Bloom filter.
2.5.0
- Add the
EstimateTableReadersMem,CurSizeAllMemTablesandBlockCacheUsageproperties, andPropertyNamefor any other property by its name. - Add
getPropertyCFto read a property of a column family. - Add
maxTotalWalSize(max_total_wal_size) toConfig, to bound the write-ahead log. - Add
createMissingColumnFamilies(create_missing_column_families) toConfig, to add column families to an existing database. - Destroy the handle of the default column family when
withDBCFopens an existing database. It was leaked, and RocksDB built with assertions aborted when the database was closed.
2.4.0
- Add
writeBufferSize(write_buffer_size) anddbWriteBufferSize(db_write_buffer_size) toConfig, to bound memtable memory.
2.3.1
- Do not call next/prev on invalid iterators (fixes assertion failure).
2.3.0
- Remove UnliftIO and remain in IO monad.
2.2.0
- Move Iterator module to plain IO.
- Properly manage read options in createSnapshot.
- Add nix flake for development.
- API allows manual database lifecycle management.
2.1.7
- Simplify author name.
2.1.6
- Refactor memory use further.
- Improve tests.
2.1.5
- Fix errors not releasing memory.
2.1.4
- Add ResourceT support for snapshots and iterators.
2.1.3
- Correct bug where database was ignoring prefix length config.
2.1.2
- Correct bug when opening database with column families.
2.1.1
- Expose ColumnFamily type.
2.1.0
- Add support for column families.
2.0.0
- Fork into a different project
- Rewrite most of the code to make it faster and easier to use
- Remove many features that I don’t need or want to maintain
1.0.1
- Add support for UTF-8 characters in a database’s path
1.0.0
- First version by Serokell
0.3x.0
- ResourceT is no longer compulsory
0.2.0
- requires LevelDB v1.7
- support for filter policy (LevelDB v1.5), either custom or using the built-in bloom filter implementation
- write batch values no longer require a
memcpyto be early-finalizer-safe (introduced in 0.1.1)
0.1.0
- memory (foreign pointers) is managed through
ResourceT. Note that this
requires to lift monadic actions inside the
MonadResourcemonad, see the examples. - links against shared library (LevelDB v1.3 or higher)
- LevelDB 1.3 API fully supported (including custom comparators, excluding custom environments)
0.0.x
- experimental releases