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) // 1 TB
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 = 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        Iter(<Self as lmdb::Cursor>::iter(self))
306    }
307
308    fn iter_start(&mut self) -> Self::Iter {
309        Iter(<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        Iter(<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 = 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        Iter(<Self as lmdb::Cursor>::iter(self))
510    }
511
512    fn iter_start(&mut self) -> Self::Iter {
513        Iter(<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        Iter(<Self as lmdb::Cursor>::iter_from(self, key))
521    }
522}
523
524pub struct Iter<'txn>(lmdb::Iter<'txn>);
525
526impl<'txn> Iterator for Iter<'txn> {
527    type Item = (&'txn [u8], &'txn [u8]);
528
529    fn next(&mut self) -> Option<(&'txn [u8], &'txn [u8])> {
530        // NOTE: This swallows possible errors, when the iterator fails to get a value.
531        // BUT I don't want to change the interface of the traits to match the Option<Result<(k, v)>> pattern.
532        // This change here was necessary, when we switched from lmdb to the lmdb-rkv crate,
533        // as lmdb simply panics in some iterator functions (because someone thought putting an unwrap there was smart).
534        // Anyway; I think the chances that the database fails *after* obtaining the iterator are low.
535        // Obtaining the iterator has a Result<T>, so if e.g. we cannot read from disk, this operation would fail.
536        // In case there really is a severe db error, all iterators will stop iterating, and the next read/write
537        // will fail with a normal error - for me this behaviour is fine.
538        flatten_opt_result(self.0.next())
539    }
540}
541
542/// A sligtly more efficient variant of `.transpose().ok().flatten()`
543///
544/// This approach avoids the allocation of intermediate `Result<Option<T>, E>` and
545/// `Option<Option<T>>` structures that happen with `.transpose().ok().flatten()`.
546/// It's unclear if those allocations would every happen in real life,
547/// as the compiler might optimize them out, and it's also unclear if this really makes a measurable
548/// difference in most use-cases..
549/// But if this variant is never *less* efficient, and *might* be more efficient, I am happy to roll with it.
550#[inline]
551fn flatten_opt_result<T, E>(opt_res: Option<Result<T, E>>) -> Option<T> {
552    match opt_res {
553        Some(Ok(value)) => Some(value),
554        _ => None,
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use rand::Rng;
562    use tempfile::tempdir;
563
564    const TEST_REPEATS: usize = 4;
565
566    fn open_tmp_lmdb() -> Lmdb {
567        let temp_dir = tempdir().unwrap();
568        let env = Lmdb::new(temp_dir.path(), 1).unwrap();
569        env
570    }
571
572    fn create_test_handle<DB: KvDatabase, Env: Db>(env: &Env) -> impl KvHandle<DB>
573    where
574        <Env as Db>::Handle: KvHandle<DB>,
575    {
576        env.create_sub_db("test").unwrap()
577    }
578
579    fn write_with_rw_txn<'env, DB: KvDatabase, Txn: RwTx<'env, DB>>(
580        txn: &mut Txn,
581        handle: &impl KvHandle<DB>,
582        key: &impl AsRef<[u8]>,
583        data: &impl AsRef<[u8]>,
584    ) -> Result<(), Error> {
585        txn.write(handle, &key, &data)?;
586        Ok(())
587    }
588
589    fn read_with_ro_txn<'env, DB: KvDatabase, Txn: RoTx<'env, DB>>(
590        txn: &Txn,
591        handle: &impl KvHandle<DB>,
592        key: &impl AsRef<[u8]>,
593    ) -> Result<Option<Vec<u8>>, Error> {
594        let buf = txn.read(handle, &key)?;
595        let out: Option<Vec<u8>> = buf.map(|v| v.to_vec());
596        Ok(out)
597    }
598
599    fn delete_with_rw_txn<'env, DB: KvDatabase, Txn: RwTx<'env, DB>>(
600        txn: &mut Txn,
601        handle: &impl KvHandle<DB>,
602        key: &impl AsRef<[u8]>,
603    ) -> Result<(), Error> {
604        txn.delete(handle, &key)?;
605        Ok(())
606    }
607
608    #[test]
609    fn read_write_delete() -> Result<(), Box<dyn std::error::Error>> {
610        let env = open_tmp_lmdb();
611        let handle = create_test_handle(&env);
612
613        for _ in 0..TEST_REPEATS {
614            let mut rng = rand::rng();
615            let test_key: Vec<u8> = (0..256).map(|_| rng.random()).collect(); // 32 byte random key
616            let test_data: Vec<u8> = (0..1048576).map(|_| rng.random()).collect(); // 1 MiB random data
617            {
618                // write test value to db
619                let mut txn = env.begin_rw_txn()?;
620                write_with_rw_txn(&mut txn, &handle, &test_key, &test_data)?;
621                Tx::commit(txn)?;
622            }
623
624            {
625                // read test value from db
626                let txn = env.begin_ro_txn()?;
627                let res = read_with_ro_txn(&txn, &handle, &test_key)?;
628                assert!(res.is_some(), "failed to read value from db");
629                assert_eq!(test_data, res.unwrap(), "data read is corrupted");
630            }
631
632            {
633                // delete test value from db
634                let mut txn = env.begin_rw_txn()?;
635                delete_with_rw_txn(&mut txn, &handle, &test_key)?;
636                Tx::commit(txn)?;
637
638                // try to read deleted value
639                let txn = env.begin_ro_txn()?;
640                let res = read_with_ro_txn(&txn, &handle, &test_key)?;
641                assert!(res.is_none(), "could read deleted value");
642            }
643        }
644        Ok(())
645    }
646
647    #[test]
648    fn not_found_is_none() -> Result<(), Box<dyn std::error::Error>> {
649        let env = open_tmp_lmdb();
650        let handle = create_test_handle(&env);
651
652        let txn = env.begin_ro_txn()?;
653        let not_found = txn.read(&handle, &[0, 0, 0, 0])?;
654        assert!(not_found.is_none());
655        Ok(())
656    }
657
658    #[test]
659    fn non_existing_db() {
660        let env = open_tmp_lmdb();
661        let db_name = "does-not-exist";
662        let res = env.open_sub_db(db_name);
663        assert!(res.is_err());
664        if let Err(e) = res {
665            match e {
666                Error::DbNotFound(name) => {
667                    assert_eq!(name, db_name, "error should include db-name")
668                }
669                _ => panic!("expected error 'DbNotFound'"),
670            }
671        }
672    }
673}