meilisearch / heed

A fully typed LMDB wrapper with minimum overhead 🐦
https://docs.rs/heed
MIT License
632 stars 57 forks source link

Is it possible to create a generic wrapper struct ? #268

Open lazybilly opened 3 months ago

lazybilly commented 3 months ago

Hi !

Thank you for this library, it looks great but I'm no Rust expert and I really struggle its lifetimes and creating a struct wrapper around it.

Is it actually possible to have a generic struct owning the env, wtxn and database ?

Like so:

pub struct Database<'p, Key, Value>
where
    Key: Ord + Clone + Debug + ?Sized + 'static,
    Value: PartialEq + 'static,
{
    env: Env,
    wtxn: RwTxn<'p>,
    db: Database<Key, Value>
}

The use case is that I have a lot of little databases with a ton of data spread among them and having to manage all env/wtxn is quite complicated and kind of messy (again I'm not a rust expert). It would really be much nicer in a nice wrapper which is possible with many other databases.

Kerollmops commented 3 months ago

Hey @lazybilly 👋

Thanks for using heed and reporting possible improvements.

We recently released the Env::static_read_txn for this specific purpose but it only works with read transaction. Can you maybe propose a PR to implement that for write transactions?

lazybilly commented 3 months ago

Hi @Kerollmops,

I'll try but I fear I might be a little too new to Rust for that.

I did actually see this static_read_txn but I couldn't figure out how to open a database with it as I sadly couldn't find any info in the docs/issues/pr/examples

let env = unsafe { EnvOpenOptions::new().open(format!("{folder}/{file}"))? }; // Create env
let txn = env.static_read_txn().unwrap(); // static read txn takes env
let db = env.open_database(&txn, None)?.unwrap(); // Can't work

env.static_read_txn eats env but then I env cannot be used to open the database, I must be missing something very obvious here !

lazybilly commented 3 months ago

Okay figured it out thanks to zed's codebase ! Very simple but didn't feel intuitive at all at first

    let env = unsafe { EnvOpenOptions::new().open("./db").unwrap() };

    let db = {
        // we will open the default unnamed database
        let mut wtxn = env.write_txn().unwrap();
        let db: Database<Str, U32<NativeEndian>> = env.create_database(&mut wtxn, None).unwrap();

        // opening a write transaction
        db.put(&mut wtxn, "seven", &7).unwrap();
        db.put(&mut wtxn, "zero", &0).unwrap();
        db.put(&mut wtxn, "five", &5).unwrap();
        db.put(&mut wtxn, "three", &3).unwrap();
        wtxn.commit().unwrap();

        db
    };

    let rtxn = env.clone().static_read_txn().unwrap();

    // Now we can use `rtxn` to read in threads etc
lazybilly commented 3 months ago

Though some examples would really be appreciated, I'm having quite a hard time with lifetimes... when trying to add functions like get/iter/.. to the wrapper. It seems that I must add 'static everywhere, does it make sense ?

Here's what I got as of right now:

pub struct Database<Key, Value>
where
    Key: Ord + Clone + Debug + ?Sized,
    Value: PartialEq,
{
    env: Env,
    txn: RoTxn<'static>,
    db: Database<Key, Value>,
}

impl<'p, Key, Value> Database<Key, Value>
where
    Key: Ord
        + Clone
        + Debug
        + ?Sized
        + 'static
        + BytesEncode<'static, EItem = Key>
        + BytesDecode<'static, DItem = &'static Key>,
    Value: PartialEq + 'static + BytesDecode<'static, DItem = &'static Value>,
{
    pub fn open(path: &Path) -> color_eyre::Result<Self> {
        fs::create_dir_all(path);

        let env = unsafe { EnvOpenOptions::new().open(path)? };

        let env = env.clone();

        let mut txn = env.write_txn()?;

        let db = env.create_database(&mut txn, None)?;

        txn.commit()?;

        let txn = env.clone().static_read_txn().unwrap();

        Ok(Self {
            env,
            txn,
            db,
        })
    }

    pub fn iter<F>(&'static self, callback: &mut F)
    where
        F: FnMut((&Key, &Value)),
    {
        self.db
            .iter(&self.txn)
            .unwrap()
            .map(|res| res.unwrap())
            .for_each(callback);
    }

    pub fn get(&'static self, key: &'static Key) -> Option<&Value> {
        self.db.get(&self.txn, key).unwrap()
    }
}

As you can see, there is 'static which really doesn't feel right