commit c8d31da1834ea29bc4e00a6cdfc748fc3ebd01b1
parent 08059713a94e7dffca071f84a936ee7124a77c27
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 24 May 2026 18:10:38 -0230
docs: flesh out README sample, add Performance section
The previous GHCi sample only covered put/get. Expand it to:
- highlight envMapSize as the first thing users should think
about (the 10 MiB default is the most common footgun)
- demonstrate a cursor range scan via cursorSeek + cursorNext
Diffstat:
| M | CHANGELOG | | | 8 | ++++---- |
| M | README.md | | | 85 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- |
2 files changed, 73 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
@@ -1,6 +1,6 @@
# Changelog
-- 0.1.0 (unreleased)
- * Initial release: minimal bindings to OpenLDAP LMDB. Supports
- environments, transactions (phantom-typed read-only vs read-write),
- databases, get / put / del, and cursor-based range scans.
+- 0.1.0 (2026-05-24)
+ * Initial release, consisting of support for LMDB environments,
+ transactions, basic row operations (get, put, del), and range scans.
+
diff --git a/README.md b/README.md
@@ -4,14 +4,12 @@

[](https://docs.ppad.tech/lmdb)
-Minimal Haskell bindings to [LMDB][lmdb] (Lightning Memory-Mapped
-Database), an embedded ACID key-value store. LMDB is single-writer,
-zero-copy on reads, MMAP-backed, and has no server process; this
-library exposes just enough of its C API to use it as a persistent
-key-value blob store.
+Bindings to [LMDB][lmdb] (Lightning Memory-Mapped Database), which is an
+embedded ACID key-value store.
-The upstream LMDB C source is vendored under `cbits/` at release
-`LMDB_0.9.33`; no external `liblmdb` is required.
+This library exposes a minimal subset of the LMDB API, mainly supporting
+LMDB environments, transactions, basic row operations (get, put, del),
+and range scans.
## Usage
@@ -21,21 +19,76 @@ A sample GHCi session:
> :set -XOverloadedStrings
> import qualified Database.LMDB as L
>
- > -- open an environment + dbi, write a value, read it back
- > let flags = L.defaultEnvFlags { L.envNoSubdir = True }
- > L.withEnv "/tmp/mydb" flags $ \env -> do
+ > -- envMapSize caps the on-disk size; the default (10 MiB) is
+ > -- intentionally small. writes past it fail with LMDBMapFull,
+ > -- so set it to something appropriate for your workload
+ > let flags = L.defaultEnvFlags
+ > { L.envNoSubdir = True
+ > , L.envMapSize = 256 * 1024 * 1024 -- 256 MiB
+ > }
+ >
+ > -- populate the env in a write transaction
+ > L.withEnv "/tmp/mydb" flags $ \env ->
> L.withWriteTxn env $ \txn -> do
> dbi <- L.openDbi txn Nothing True
- > L.put txn dbi "hello" "world"
+ > L.put txn dbi "apple" "red"
+ > L.put txn dbi "banana" "yellow"
+ > L.put txn dbi "cherry" "red"
+ >
+ > -- point lookup
+ > L.withEnv "/tmp/mydb" flags $ \env ->
+ > L.withReadTxn env $ \txn -> do
+ > dbi <- L.openDbi txn Nothing False
+ > L.get txn dbi "banana"
+ Just "yellow"
+ >
+ > -- range scan; every entry with key >= "b"
+ > L.withEnv "/tmp/mydb" flags $ \env ->
> L.withReadTxn env $ \txn -> do
> dbi <- L.openDbi txn Nothing False
- > L.get txn dbi "hello"
- Just "world"
+ > L.withCursor txn dbi $ \cur -> do
+ > let go acc Nothing = pure (reverse acc)
+ > go acc (Just kv) = L.cursorNext cur >>= go (kv : acc)
+ > L.cursorSeek cur "b" >>= go []
+ [("banana","yellow"),("cherry","red")]
```
-Read-only and read-write transactions are distinguished at the type
-level via a phantom parameter, so writing from a read-only transaction
-is a compile-time error.
+## Performance
+
+Current benchmark figures on an M4 Silicon MacBook Air look like (use
+`cabal bench` to run the benchmark suite):
+
+```
+ benchmarking put/1k inserts (one txn)
+ time 559.5 μs (557.5 μs .. 562.0 μs)
+ 1.000 R² (0.999 R² .. 1.000 R²)
+ mean 561.8 μs (559.6 μs .. 567.0 μs)
+ std dev 11.34 μs (5.445 μs .. 21.32 μs)
+
+ benchmarking put/10k inserts (one txn)
+ time 4.026 ms (4.016 ms .. 4.037 ms)
+ 1.000 R² (1.000 R² .. 1.000 R²)
+ mean 4.013 ms (4.005 ms .. 4.022 ms)
+ std dev 26.80 μs (22.08 μs .. 36.75 μs)
+
+ benchmarking get/1k random hits
+ time 307.1 μs (306.3 μs .. 308.1 μs)
+ 1.000 R² (1.000 R² .. 1.000 R²)
+ mean 308.0 μs (307.3 μs .. 309.4 μs)
+ std dev 3.368 μs (1.893 μs .. 5.357 μs)
+
+ benchmarking get/10k random hits
+ time 2.882 ms (2.872 ms .. 2.893 ms)
+ 1.000 R² (1.000 R² .. 1.000 R²)
+ mean 2.884 ms (2.879 ms .. 2.889 ms)
+ std dev 16.25 μs (13.40 μs .. 21.54 μs)
+
+ benchmarking cursor/scan 10k entries
+ time 1.249 ms (1.245 ms .. 1.258 ms)
+ 0.999 R² (0.996 R² .. 1.000 R²)
+ mean 1.251 ms (1.245 ms .. 1.281 ms)
+ std dev 35.00 μs (5.595 μs .. 82.99 μs)
+```
## Documentation