DEV Community

loading...
Cover image for A Command Line Key-Value Data Store using the Rust Programming Language.

A Command Line Key-Value Data Store using the Rust Programming Language.

John Idogun
Budding Full-stack Software Engineer with experience building web applications using Python(Django and Flask), Rust, and JavaScript.
Updated on ・4 min read

Prelude

Rust is an imperative, super fast, and type-safe programming language that empowers you — a Software Engineer — "to reach farther, to program with confidence in a wider variety of domains than you did before." No wonder it has consistently maintained its deserved spot as the most loved programming language for half a decade!

What we are building

Using barebone rust code, we will be building a simple command line key-value data store like Redis. It should take in two command line arguments and assign the first as the key while the second, value.

If installed on your machine, it can be used as follows:

┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ rust-kvstore needle haystack
Enter fullscreen mode Exit fullscreen mode

This should create a file, aptly named kv.db. It's content can then be read:

┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ cat kv.db
Enter fullscreen mode Exit fullscreen mode

Whose output should look like:

───────┬─────────────────────────────────────────────────────
       │ File: kv.db
───────┼─────────────────────────────────────────────────────
   1   │ needle  haystack
───────┴─────────────────────────────────────────────────────
Enter fullscreen mode Exit fullscreen mode

However, if you have the source files, you can simply build and run it using:

┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ cargo run needle haystack
Enter fullscreen mode Exit fullscreen mode

This is simply for learning sake and no other motive is intended.

DECLAIMER

This example is based off of a two-part tutorial anchored by the beloved Ryan Levick. The only significant additions are: the use of a Vec<String> instead of Iterator<Item = String>; fixing this bug

thread 'main' panicked at 'Corrupt database: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Enter fullscreen mode Exit fullscreen mode


by checking if kv.db already exists and if not create it using Rust's PathBuf standard library; using a more efficient file reader; and using split_once() instead of rsplit() among others.
It is highly recommended to check out the awesome livestreams on youtube. I must confess, it was a total tear down and dissection.

Assumptions

It is assumed that you have read The Rust book to some extent or have checked out the awesome livestreams on youtube by Ryan Levick.

Source code

As usual, you can get the full version of the source files for this article on

GitHub logo Sirneij / rust-kvstore

A simple key-value store such as Redis, implemented using the rust programming language

Key value store

. Just clone it:

┌──(sirneij@sirneij)-[~/Documents/Projects]
└─$[sirneij@sirneij Projects]$  git clone https://github.com/Sirneij/rust-kvstore.git
Enter fullscreen mode Exit fullscreen mode

and open it in your favourite text editor, mine is vs code.

┌──(sirneij@sirneij)-[~/Documents/Projects]
└─$[sirneij@sirneij Projects]$ cd rust-kvstore && code .
Enter fullscreen mode Exit fullscreen mode

Proper implementation

Going by the assumptions made above, I will only point out some of my inputs.

  • Taking arguments as vectors:

Since our little project wants to get two command line arguments, Rust provides a function args() which can be found in the std::env library. This function returns an iterator of the command line arguments. The .collect() converts the returned iterator into a vector. Its implementation for this project looks this way:

fn main(){
    let args: Vec<String> = std::env::args().collect();
    let key = &args[1];
    let value = &args[2];
}
Enter fullscreen mode Exit fullscreen mode

It should be noted that &args[0] gives the path to our executable which in this case should be "target/debug/rust-kvstore". You can see what is in args by printing it to the console using println!() macro:

fn main(){
    let args: Vec<String> = std::env::args().collect();
    println!("{:?}", args);
    let key = &args[1];
    let value = &args[2];
}
Enter fullscreen mode Exit fullscreen mode

You should see something like:

["target/debug/rust-kvstore", "needle", "haystack"]
Enter fullscreen mode Exit fullscreen mode

if you pass needle haystack as arguments using cargo run like so:

┌──(sirneij@sirneij)-[~/Documents/Projects/rust-kvstore]
└─$[sirneij@sirneij rust-kvstore]$ cargo run needle haystack
Enter fullscreen mode Exit fullscreen mode

Since we are only concerned with arguments we passed, which starts from &args[1], we overlooked &args[0].

  • Fixing "No such file or directory" bug: If kv.db is not manually created or not present at the start of the program's usage, an error of this form will surface:
thread 'main' panicked at 'Corrupt database: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Enter fullscreen mode Exit fullscreen mode

To fix this, we need to check whether or not kv.db has been created. If not create it on the fly. To accomplish this, we use this awesome std::path::PathBuf library in our "constructor", new().

impl Database {
   fn new() -> Result<Database, std::io::Error> {
        ...
        let mut contents = String::new();
        let path = PathBuf::from("kv.db");
        if path.exists() {
            let file = std::fs::File::open(path)?;
            let mut buf_reader = std::io::BufReader::new(file);
            buf_reader.read_to_string(&mut contents)?;
        } else {
            std::fs::File::create("kv.db")?;
        }
        ...
   }
}
Enter fullscreen mode Exit fullscreen mode
  • Using the more efficient std::io::BufReader
    It can also be seen in the snippet above that instead of using the std::read_to_string, we opted for the more efficient std::io::BufReader::new(file);

  • split_once() implemented:
    When Ryan was livestreaming, split_once() was only available in the nightly version of Rust so he opted for rsplit(). However, I think it is stable now and the full implementation is shown as follows:

     ...
     for line in contents.lines() {
            let (key, value) = line.split_once("\t").expect("Corrupt database");
            map.insert(key.to_string(), value.to_string());
        }
     ...
Enter fullscreen mode Exit fullscreen mode

That's it! Nifty and awesome. To build the project from scratch, code along with Ryan Levick..

Kindly drop your comments, reactions and suggestions. Make me a better writer.

References

Attributions

Discussion (0)