MiniCouchDB in Rust
3 min read

MiniCouchDB in Rust

Recently we had a Hack Week, so I took some inspiration from mini-redis and tried to build a mini-CouchDB in Rust.
MiniCouchDB in Rust
Photo by Zan / Unsplash

Recently we had a Hack Week, where we could work on anything we liked for the week. Since I’m enjoying programming in Rust, I took some inspiration from mini-redis and tried to build a mini-CouchDB in Rust.

In case you don’t know, the next big release of CouchDB, version 4.x, will be built on top of FoundationDB (FDB). FoundationDB is a distributed, transactional Key/Value store. For this hack week, I decided to follow the design of CouchDB implemented on FoundationDB and see if I could get my Rust implementation to read the data that CouchDB stores in FoundationDB.

The code has three main sections. The Fdb layer, error handling, and the HTTP layer.

FoundationDB Layer

The first step was to implement reading from FoundationDB (FDB). I was really interested to see how reading and unpacking the data from FoundationDB in Rust would compare to doing the same in Erlang.

The FoundationDB Rust client doesn’t yet have support for the directory layer in FDB. However, it turned out to be quite trivial to read the directory layer section to determine how to read from CouchDB. What I found interesting was that packing/unpacking data from FoundationDB with the Rust client looked very similar yo Erlang. Below is the code to pack/unpack the database name in Erlang:

Prefix = erlfdb_tuple:pack({?ALL_DBS}, LayerPrefix),
{DbName} = erlfdb_tuple:unpack(K, Prefix),

In Rust it would look like this:

let (start_key, end_key) = fdb::pack_key_range(&ALL_DBS, couch_directory);
let (_, _, db_bytes): (Element, Element, Bytes) = unpack(kv.key())?;

The obvious difference being that in Rust I have to specify a type for each field in the tuple. Initially doing it like this was fine, but it felt way to basic and I wanted to push my knowledge of types in Rust to see if I can implement a better unpacking design. This lead me to defining types that I could convert the FoundationDB Bytes type into. Here is an example:

type EncodedRev = (i16, Bytes);
type EncodedChangeValue = (Bytes, bool, EncodedRev);

pub struct Rev(String);

impl From<(i16, Bytes)> for Rev {
    fn from((num, str): (i16, Bytes)) -> Self {
        Rev(format!("{}-{}", num, hex::encode(str.as_ref())))

This meant that I could then take an EncodedRev variable and convert it straight into a Rev:

let rev_tuple: EncodedRev = unpack(kv.value())?;
let rev: Rev = rev_tuple.into();

This worked quite well. The next step, which I unfortunately did not have time to do, would be to take the types a step further and instead of having the Tuple types defined above, rather implement the Unpack trait for the Rev so that the unpack would look like this:

let rev: Rev = unpack(kv.value())?;

This makes the code a lot simpler to read and would be making better use of Rust’s powerful type system.

Http Layer

For the Http Layer , I decided to use I’ve used Hyper before but wanted to use something with a bit more functionality. Warp is a functional web framework that handles requests by filtering the request to the correct route. It is well written and is very powerful. I found it was a pretty big learning curve in the beginning and I had to spend a good day and a bit to fully understand what I was doing. The community in the Tokio discord channel were incredibly patient and helped me overcome a few sticking points. I’m still a little bit on the fence on whether I would use it again. It is really powerful but is at times frustrating to get working.

Error handling

The final part of miniCouchDB was to implement some error handling. Rust’s error handling is something that amazes me every time. It’s easy to set up and use throughout the app. I was able to quickly implement a common CouchError that all FDB errors, standard errors, and custom errors could be converted too. I then implemented a handle_rejection function that would map all CouchError’s into HTTP error codes and messages. This was all easy to do and I think the code is easy to read and maintain.

In the end I managed to implement the _all_dbs, db_info , _all_docs, and _changes endpoint. For a week and a bit of time, I thought that was pretty good. I learnt a lot on how to use the FoundationDB-rs library and had a ton of fun writing code in Rust again. I did find I still need to learn more about Rust’s advanced topics like Trait objects and get a better understanding of lifetimes and borrowing.

Rust is a language I would like to use more. The ecosystem is really good and the community is very welcoming. If you haven’t yet dipped your toes into exploring Rust. I can’t recommend it enough. There are plenty of great resources to get started and the Rust book is really helpful.

Finally, thanks to Peng Hui who helped build this with me.