borderless_kv_store/backend/lmdb.rs
1use crate::{CursorOp, Error, KvDatabase, KvHandle, RoCursor, RoTx, RwCursor, RwTx, ToCursorOp};
2use lmdb::EnvironmentFlags;
3use lmdb_sys::{MDB_FIRST, MDB_GET_CURRENT, MDB_LAST, MDB_NEXT, MDB_PREV};
4use std::{path::Path, sync::Arc};
5
6use crate::{Db, RawRead, RawWrite, Tx};
7
8pub use lmdb::Database as DbPtr;
9
10/// Converts LMDB-specific errors (`lmdb::Error`) into the database interface `Error` type.
11///
12/// This implementation maps LMDB errors to a meaningful representation in the library context,
13/// allowing better handling of database errors.
14///
15/// ### Notes:
16/// - The `NotFound` error is deliberately not converted to the custom `Error` type because it is
17/// represented by the `Option` type in the interface itself.
18/// - For errors like `Other`, additional contextual information is included for debugging purposes.
19impl From<lmdb::Error> for Error {
20 fn from(value: lmdb::Error) -> Error {
21 match value {
22 lmdb::Error::KeyExist => Error::KeyExist,
23 lmdb::Error::NotFound => panic!("Wrong implementation, the key not found error is pictured by the option type in the interface iteself."),
24 lmdb::Error::PageNotFound => Error::Other("Database page not found".to_string()),
25 lmdb::Error::Corrupted => Error::Corrupted,
26 lmdb::Error::Panic => Error::Other("panic occured".to_string()),
27 lmdb::Error::VersionMismatch => Error::Other("version mismatch".to_string()),
28 lmdb::Error::Invalid => Error::InvalidArgument,
29 lmdb::Error::MapResized => Error::Other("map resized".to_string()),
30 lmdb::Error::Incompatible => Error::Other("incopatible format".to_string()),
31 lmdb::Error::BadRslot => Error::Other("bad reader slot".to_string()),
32 lmdb::Error::BadTxn => Error::Other("bad transaction".to_string()),
33 lmdb::Error::BadValSize => Error::Other("bad value size".to_string()),
34 lmdb::Error::BadDbi => Error::Other("bad database index".to_string()),
35 lmdb::Error::Other(code) => Error::Other(format!("unknown lmdb error code: {}", code)),
36 e => Error::Busy(e.to_string())
37 }
38 }
39}
40
41impl ToCursorOp<lmdb::Database> for CursorOp {
42 fn to_op(&self) -> u32 {
43 match self {
44 CursorOp::First => MDB_FIRST,
45 CursorOp::Last => MDB_LAST,
46 CursorOp::Next => MDB_NEXT,
47 CursorOp::Prev => MDB_PREV,
48 CursorOp::Current => MDB_GET_CURRENT,
49 }
50 }
51}
52
53/// Marks LMDB's `Database` type as implementing the `KvDatabase` trait.
54///
55/// This allows LMDB's native database type to integrate with the `KvDatabase` trait, enabling
56/// it to be used seamlessly with the broader key-value store interface.
57impl KvDatabase for lmdb::Database {}
58
59/// Implements the `KvHandle` trait for `lmdb::Database`, providing database access.
60///
61/// This implementation ensures compatibility with the key-value interface by exposing the
62/// underlying LMDB database.
63impl KvHandle<lmdb::Database> for lmdb::Database {
64 /// Returns a reference to the underlying LMDB database.
65 ///
66 /// ### Returns:
67 /// - A reference to the database managed by this handle.
68 fn db(&self) -> &lmdb::Database {
69 self
70 }
71}
72
73/// Represents an LMDB environment for managing databases and transactions.
74///
75/// The `Lmdb` provides a high-level abstraction over the LMDB `Environment`,
76/// enabling the creation and management of databases, as well as read-only and
77/// read-write transactions.
78#[derive(Clone)]
79pub struct Lmdb {
80 env: Arc<lmdb::Environment>,
81}
82
83impl Lmdb {
84 /// Initializes a new LMDB environment at the specified path with a given maximum number of databases.
85 ///
86 /// ### Parameters:
87 /// - `path`: The filesystem path where the LMDB environment will be created or accessed.
88 /// - `max_dbs`: The maximum number of named databases that can be created within this environment.
89 ///
90 /// ### Returns:
91 /// - `Ok(Lmdb)` if the environment was successfully initialized.
92 /// - `Err(Error)` if an error occurred during initialization.
93 pub fn new(path: &Path, max_dbs: u32) -> Result<Lmdb, Error> {
94 // We can do some further optimizations, if we want to increase the performance:
95 let flags = EnvironmentFlags::default()
96 | EnvironmentFlags::WRITE_MAP // Faster writes, backed by OS memory mapping. Safe on modern OSes. (incompatible with nested transactions !)
97 | EnvironmentFlags::NO_META_SYNC; // Skips metadata sync — tiny risk on power loss, big write speed boost.
98
99 let env = lmdb::Environment::new()
100 .set_max_dbs(max_dbs)
101 // NOTE: we have to maintain the map size
102 // in future. A mechanism to increase this size
103 // is a good idea.
104 .set_map_size(1_099_511_627_776)
105 .set_max_readers(2048) // allow more concurrent readers
106 .set_flags(flags)
107 .open(path)?;
108
109 Ok(Lmdb { env: Arc::new(env) })
110 }
111}
112
113/// Implements the `Db` trait for `Lmdb`.
114///
115/// This allows `Lmdb` to serve as a fully functional key-value environment
116/// compatible with the application's interface.
117impl Db for Lmdb {
118 type DB = lmdb::Database;
119 type Handle = lmdb::Database;
120 type RoTx<'env> = lmdb::RoTransaction<'env>;
121 type RwTx<'env> = lmdb::RwTransaction<'env>;
122
123 /// Opens a database by name or the default database if the name is "default".
124 ///
125 /// ### Parameters:
126 /// - `name`: The name of the database to open, or "default" to open the unnamed default database.
127 ///
128 /// ### Returns:
129 /// - `Ok(Handle<'env>)` containing a handle to the opened database.
130 /// - `Err(Error)` if the database could not be opened.
131 fn open_sub_db(&self, name: &str) -> Result<Self::Handle, Error> {
132 // NOTE: This can also cause Error::NotFound
133 let res = if name.to_ascii_lowercase() == "default" {
134 self.env.open_db(None)
135 } else {
136 self.env.open_db(Some(name))
137 };
138
139 let handle = match res {
140 Ok(db) => db,
141 Err(lmdb::Error::NotFound) => return Err(Error::DbNotFound(name.to_string())),
142 Err(e) => return Err(Error::from(e)),
143 };
144
145 Ok(handle)
146 }
147
148 /// Create a database by name or the default database if the name is "default".
149 ///
150 /// ### Parameters:
151 /// - `name`: The name of the database to open, or "default" to open the unnamed default database.
152 ///
153 /// ### Returns:
154 /// - `Ok(Handle<'env>)` containing a handle to the opened database.
155 /// - `Err(Error)` if the database could not be opened.
156 fn create_sub_db(&self, name: &str) -> Result<Self::Handle, Error> {
157 let handle = if name == "default" {
158 self.env.create_db(None, lmdb::DatabaseFlags::empty())?
159 } else {
160 self.env
161 .create_db(Some(name), lmdb::DatabaseFlags::empty())?
162 };
163
164 Ok(handle)
165 }
166
167 /// Begins a new read-only transaction within the environment.
168 ///
169 /// ### Returns:
170 /// - `Ok(RoTx<'env>)` containing the transaction object.
171 /// - `Err(Error)` if the transaction could not be started.
172 fn begin_ro_txn(&self) -> Result<Self::RoTx<'_>, Error> {
173 let txn = self.env.begin_ro_txn()?;
174 Ok(txn)
175 }
176
177 /// Begins a new read-write transaction within the environment.
178 ///
179 /// ### Returns:
180 /// - `Ok(RwTx<'env>)` containing the transaction object.
181 /// - `Err(Error)` if the transaction could not be started.
182 fn begin_rw_txn(&self) -> Result<Self::RwTx<'_>, Error> {
183 let txn = self.env.begin_rw_txn()?;
184 Ok(txn)
185 }
186}
187
188/// Implements the `Tx` trait for LMDB's read-only transactions (`RoTransaction`).
189///
190/// This trait provides methods to manage the lifecycle of read-only transactions,
191/// including committing and aborting.
192impl<'env> Tx for lmdb::RoTransaction<'env> {
193 /// Commits the transaction, making all changes visible to other transactions.
194 ///
195 /// ### Returns:
196 /// - `Ok(())` if the transaction was successfully committed.
197 /// - `Err(Error)` if an error occurred during the commit operation.
198 fn commit(self) -> Result<(), Error> {
199 <Self as lmdb::Transaction>::commit(self)?;
200 Ok(())
201 }
202
203 /// Aborts the transaction, discarding all changes made during its lifetime.
204 ///
205 /// ### Notes:
206 /// - This method ensures that the transaction is cleanly terminated without affecting the database.
207 fn abort(self) {
208 <Self as lmdb::Transaction>::abort(self);
209 }
210}
211
212/// Implements the `RawRead` trait for LMDB's read-only transaction (`RoTransaction`).
213///
214/// This allows performing read operations within the context of a read-only transaction.
215/// It retrieves data from the database based on a specified key.
216///
217/// ### Methods:
218/// - `read`: Fetches the value associated with a given key in the specified database.
219impl<'env> RawRead<'env, lmdb::Database> for lmdb::RoTransaction<'env> {
220 /// Reads a value from the database associated with the provided key.
221 ///
222 /// ### Parameters:
223 /// - `db`: A handle to the database to query.
224 /// - `key`: The key to search for in the database.
225 ///
226 /// ### Returns:
227 /// - `Ok(Some(&[u8]))`: If the key exists, returns a reference to the value.
228 /// - `Ok(None)`: If the key is not found.
229 /// - `Err(Error)`: If an error occurs during the operation.
230 fn read(
231 &self,
232 db: &impl KvHandle<lmdb::Database>,
233 key: &impl AsRef<[u8]>,
234 ) -> Result<Option<&[u8]>, Error> {
235 let res = <Self as lmdb::Transaction>::get(self, *db.db(), key);
236
237 let data = match res {
238 Ok(buf) => Some(buf),
239 Err(lmdb::Error::NotFound) => None,
240 Err(e) => return Err(Error::from(e)),
241 };
242
243 Ok(data)
244 }
245}
246
247/// Implements the `RoTx` trait for LMDB's read-only transactions.
248///
249/// This provides additional methods specific to read-only transactions, such as creating cursors.
250impl<'env> RoTx<'env, lmdb::Database> for lmdb::RoTransaction<'env> {
251 /// Type definition for the cursor used in read-only transactions.
252 type Cursor<'txn> = lmdb::RoCursor<'txn>
253 where
254 Self: 'txn;
255
256 /// Creates a cursor for iterating over the database in the context of a read-only transaction.
257 ///
258 /// ### Parameters:
259 /// - `db`: A handle to the database for which the cursor is created.
260 ///
261 /// ### Returns:
262 /// - `Ok(Cursor)`: If the cursor is successfully created.
263 /// - `Err(Error)`: If an error occurs.
264 fn ro_cursor<'txn>(
265 &'txn self,
266 db: &impl KvHandle<lmdb::Database>,
267 ) -> Result<Self::Cursor<'txn>, Error> {
268 let cursor = <Self as lmdb::Transaction>::open_ro_cursor(self, *db.db())?;
269 Ok(cursor)
270 }
271}
272
273/// Implements the `RoCursor` trait for LMDB's read-only cursors.
274///
275/// Provides methods to iterate over the key-value pairs in the database.
276impl<'txn> RoCursor<'txn, lmdb::Database> for lmdb::RoCursor<'txn> {
277 /// Type definition for the iterator used in the cursor.
278 type Iter = lmdb::Iter<'txn>;
279
280 fn get<K, V>(
281 &self,
282 key: Option<&K>,
283 value: Option<&V>,
284 op: impl ToCursorOp<lmdb::Database>,
285 ) -> Result<(Option<&'txn [u8]>, &'txn [u8]), Error>
286 where
287 K: AsRef<[u8]>,
288 V: AsRef<[u8]>,
289 {
290 let res = <Self as lmdb::Cursor>::get(
291 self,
292 key.map(|k| k.as_ref()),
293 value.map(|v| v.as_ref()),
294 op.to_op(),
295 )?;
296
297 Ok(res)
298 }
299
300 /// Creates an iterator over the database starting from the current cursor position.
301 ///
302 /// ### Returns:
303 /// - An iterator over key-value pairs in the database.
304 fn iter(&mut self) -> Self::Iter {
305 <Self as lmdb::Cursor>::iter(self)
306 }
307
308 fn iter_start(&mut self) -> Self::Iter {
309 <Self as lmdb::Cursor>::iter_start(self)
310 }
311
312 fn iter_from<K>(&mut self, key: &K) -> Self::Iter
313 where
314 K: AsRef<[u8]>,
315 {
316 <Self as lmdb::Cursor>::iter_from(self, key)
317 }
318}
319
320/// Implements the `Tx` trait for LMDB's read-write transactions.
321///
322/// Provides methods to manage the lifecycle of read-write transactions, such as committing or aborting.
323impl<'env> Tx for lmdb::RwTransaction<'env> {
324 /// Commits the transaction, applying all changes to the database.
325 ///
326 /// ### Returns:
327 /// - `Ok(())`: If the transaction was successfully committed.
328 /// - `Err(Error)`: If an error occurs during the commit operation.
329 fn commit(self) -> Result<(), Error> {
330 <Self as lmdb::Transaction>::commit(self)?;
331 Ok(())
332 }
333
334 /// Aborts the transaction, discarding all changes made within it.
335 ///
336 /// ### Notes:
337 /// - This ensures no changes from the transaction are persisted in the database.
338 fn abort(self) {
339 <Self as lmdb::Transaction>::abort(self);
340 }
341}
342
343/// Implements the `RawRead` trait for LMDB's read-write transaction (`RwTransaction`).
344///
345/// Enables reading data within the context of a read-write transaction.
346impl<'env> RawRead<'env, lmdb::Database> for lmdb::RwTransaction<'env> {
347 /// Reads a value from the database associated with the provided key.
348 ///
349 /// ### Parameters:
350 /// - `db`: A handle to the database to query.
351 /// - `key`: The key to search for in the database.
352 ///
353 /// ### Returns:
354 /// - `Ok(Some(&[u8]))`: If the key exists, returns a reference to the value.
355 /// - `Ok(None)`: If the key is not found.
356 /// - `Err(Error)`: If an error occurs during the operation.
357 fn read(
358 &self,
359 db: &impl KvHandle<lmdb::Database>,
360 key: &impl AsRef<[u8]>,
361 ) -> Result<Option<&[u8]>, Error> {
362 let res = <Self as lmdb::Transaction>::get(self, *db.db(), key);
363
364 let data = match res {
365 Ok(buf) => Some(buf),
366 Err(lmdb::Error::NotFound) => None,
367 Err(e) => return Err(Error::from(e)),
368 };
369
370 Ok(data)
371 }
372}
373
374/// Implements the `RawWrite` trait for LMDB's read-write transaction (`RwTransaction`).
375///
376/// Enables writing and deleting data within the context of a read-write transaction.
377impl<'env> RawWrite<'env, lmdb::Database> for lmdb::RwTransaction<'env> {
378 /// Writes a key-value pair to the database.
379 ///
380 /// ### Parameters:
381 /// - `db`: A handle to the database where the key-value pair will be stored.
382 /// - `key`: The key to store.
383 /// - `data`: The value associated with the key.
384 ///
385 /// ### Returns:
386 /// - `Ok(())`: If the write operation was successful.
387 /// - `Err(Error)`: If an error occurs during the operation.
388 fn write(
389 &mut self,
390 db: &impl KvHandle<lmdb::Database>,
391 key: &impl AsRef<[u8]>,
392 data: &impl AsRef<[u8]>,
393 ) -> Result<(), Error> {
394 self.put(*db.db(), key, &data, lmdb::WriteFlags::empty())?;
395 Ok(())
396 }
397
398 /// Deletes a key-value pair from the database.
399 ///
400 /// ### Parameters:
401 /// - `db`: A handle to the database from which the key-value pair will be deleted.
402 /// - `key`: The key to delete.
403 ///
404 /// ### Returns:
405 /// - `Ok(())`: If the deletion was successful.
406 /// - `Err(Error)`: If an error occurs during the operation.
407 fn delete(
408 &mut self,
409 db: &impl KvHandle<lmdb::Database>,
410 key: &impl AsRef<[u8]>,
411 ) -> Result<(), Error> {
412 let res = self.del(*db.db(), key, None);
413
414 match res {
415 Ok(_) => Ok(()),
416 Err(lmdb::Error::NotFound) => Ok(()),
417 Err(e) => Err(Error::from(e)),
418 }
419 }
420}
421
422/// Implements the `RwTx` trait for LMDB's read-write transactions.
423///
424/// Provides methods to create cursors and nested transactions within the context of a read-write transaction.
425impl<'env> RwTx<'env, lmdb::Database> for lmdb::RwTransaction<'env> {
426 /// Type definition for cursors used in read-write transactions.
427 type Cursor<'txn> = lmdb::RwCursor<'txn>
428 where
429 Self: 'txn;
430
431 /// Type definition for nested read-write transactions.
432 type RwTx<'txn> = lmdb::RwTransaction<'txn>
433 where
434 Self: 'txn;
435
436 /// Creates a cursor for iterating over the database in the context of a read-write transaction.
437 ///
438 /// ### Parameters:
439 /// - `db`: A handle to the database for which the cursor is created.
440 ///
441 /// ### Returns:
442 /// - `Ok(Cursor)`: If the cursor is successfully created.
443 /// - `Err(Error)`: If an error occurs.
444 fn rw_cursor<'txn>(
445 &'txn mut self,
446 db: &impl KvHandle<lmdb::Database>,
447 ) -> Result<Self::Cursor<'txn>, Error> {
448 let cursor = self.open_rw_cursor(*db.db())?;
449 Ok(cursor)
450 }
451
452 /// Begins a nested transaction within the current transaction.
453 ///
454 /// ### Returns:
455 /// - `Ok(RwTx<'txn>)`: If the nested transaction is successfully started.
456 /// - `Err(Error)`: If an error occurs during the operation.
457 fn nested_txn(&mut self) -> Result<Self::RwTx<'_>, Error> {
458 let ntx = self.begin_nested_txn()?;
459 Ok(ntx)
460 }
461}
462
463/// Implements the `RwCursor` trait for LMDB's read-write cursors.
464///
465/// Provides methods to iterate over the key-value pairs in the database.
466impl<'txn> RwCursor<'txn, lmdb::Database> for lmdb::RwCursor<'txn> {
467 /// Type definition for the iterator used in the cursor.
468 type Iter = lmdb::Iter<'txn>;
469
470 fn get<K, V>(
471 &self,
472 key: Option<&K>,
473 value: Option<&V>,
474 op: impl ToCursorOp<lmdb::Database>,
475 ) -> Result<(Option<&'txn [u8]>, &'txn [u8]), Error>
476 where
477 K: AsRef<[u8]>,
478 V: AsRef<[u8]>,
479 {
480 let res = <Self as lmdb::Cursor>::get(
481 self,
482 key.map(|k| k.as_ref()),
483 value.map(|v| v.as_ref()),
484 op.to_op(),
485 )?;
486
487 Ok(res)
488 }
489
490 fn put<K, V>(&mut self, key: &K, value: &V) -> Result<(), Error>
491 where
492 K: AsRef<[u8]>,
493 V: AsRef<[u8]>,
494 {
495 self.put(key, value, lmdb::WriteFlags::empty())?;
496 Ok(())
497 }
498
499 fn del(&mut self) -> Result<(), Error> {
500 self.del(lmdb::WriteFlags::empty())?;
501 Ok(())
502 }
503
504 /// Creates an iterator over the database starting from the current cursor position.
505 ///
506 /// ### Returns:
507 /// - An iterator over key-value pairs in the database.
508 fn iter(&mut self) -> Self::Iter {
509 <Self as lmdb::Cursor>::iter(self)
510 }
511
512 fn iter_start(&mut self) -> Self::Iter {
513 <Self as lmdb::Cursor>::iter_start(self)
514 }
515
516 fn iter_from<K>(&mut self, key: &K) -> Self::Iter
517 where
518 K: AsRef<[u8]>,
519 {
520 <Self as lmdb::Cursor>::iter_from(self, key)
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use rand::Rng;
528 use tempfile::tempdir;
529
530 const TEST_REPEATS: usize = 4;
531
532 fn open_tmp_lmdb() -> Lmdb {
533 let temp_dir = tempdir().unwrap();
534 let env = Lmdb::new(temp_dir.path(), 1).unwrap();
535 env
536 }
537
538 fn create_test_handle<DB: KvDatabase, Env: Db>(env: &Env) -> impl KvHandle<DB>
539 where
540 <Env as Db>::Handle: KvHandle<DB>,
541 {
542 env.create_sub_db("test").unwrap()
543 }
544
545 fn write_with_rw_txn<'env, DB: KvDatabase, Txn: RwTx<'env, DB>>(
546 txn: &mut Txn,
547 handle: &impl KvHandle<DB>,
548 key: &impl AsRef<[u8]>,
549 data: &impl AsRef<[u8]>,
550 ) -> Result<(), Error> {
551 txn.write(handle, &key, &data)?;
552 Ok(())
553 }
554
555 fn read_with_ro_txn<'env, DB: KvDatabase, Txn: RoTx<'env, DB>>(
556 txn: &Txn,
557 handle: &impl KvHandle<DB>,
558 key: &impl AsRef<[u8]>,
559 ) -> Result<Option<Vec<u8>>, Error> {
560 let buf = txn.read(handle, &key)?;
561 let out: Option<Vec<u8>> = buf.map(|v| v.to_vec());
562 Ok(out)
563 }
564
565 fn delete_with_rw_txn<'env, DB: KvDatabase, Txn: RwTx<'env, DB>>(
566 txn: &mut Txn,
567 handle: &impl KvHandle<DB>,
568 key: &impl AsRef<[u8]>,
569 ) -> Result<(), Error> {
570 txn.delete(handle, &key)?;
571 Ok(())
572 }
573
574 #[test]
575 fn read_write_delete() -> Result<(), Box<dyn std::error::Error>> {
576 let env = open_tmp_lmdb();
577 let handle = create_test_handle(&env);
578
579 for _ in 0..TEST_REPEATS {
580 let mut rng = rand::rng();
581 let test_key: Vec<u8> = (0..256).map(|_| rng.random()).collect(); // 32 byte random key
582 let test_data: Vec<u8> = (0..1048576).map(|_| rng.random()).collect(); // 1 MiB random data
583 {
584 // write test value to db
585 let mut txn = env.begin_rw_txn()?;
586 write_with_rw_txn(&mut txn, &handle, &test_key, &test_data)?;
587 Tx::commit(txn)?;
588 }
589
590 {
591 // read test value from db
592 let txn = env.begin_ro_txn()?;
593 let res = read_with_ro_txn(&txn, &handle, &test_key)?;
594 assert!(res.is_some(), "failed to read value from db");
595 assert_eq!(test_data, res.unwrap(), "data read is corrupted");
596 }
597
598 {
599 // delete test value from db
600 let mut txn = env.begin_rw_txn()?;
601 delete_with_rw_txn(&mut txn, &handle, &test_key)?;
602 Tx::commit(txn)?;
603
604 // try to read deleted value
605 let txn = env.begin_ro_txn()?;
606 let res = read_with_ro_txn(&txn, &handle, &test_key)?;
607 assert!(res.is_none(), "could read deleted value");
608 }
609 }
610 Ok(())
611 }
612
613 #[test]
614 fn not_found_is_none() -> Result<(), Box<dyn std::error::Error>> {
615 let env = open_tmp_lmdb();
616 let handle = create_test_handle(&env);
617
618 let txn = env.begin_ro_txn()?;
619 let not_found = txn.read(&handle, &[0, 0, 0, 0])?;
620 assert!(not_found.is_none());
621 Ok(())
622 }
623
624 #[test]
625 fn non_existing_db() {
626 let env = open_tmp_lmdb();
627 let db_name = "does-not-exist";
628 let res = env.open_sub_db(db_name);
629 assert!(res.is_err());
630 if let Err(e) = res {
631 match e {
632 Error::DbNotFound(name) => {
633 assert_eq!(name, db_name, "error should include db-name")
634 }
635 _ => panic!("expected error 'DbNotFound'"),
636 }
637 }
638 }
639}