🦀 New Rust Concepts
- Variables: How Rust handles data
- Mutability: When data can change
- Structs: Creating custom types
- HashMap: Rust’s key-value collection
- Option: Handling missing values
Today we’re building the foundation of FerrisDB - a simple key-value store! Think of it like a super-fast notebook where you can store and retrieve information using a unique key.
graph LR A[Your App] -->|"set('user:123', 'Alice')"| B[Key-Value Store] B -->|"get('user:123')"| A B -->|"Returns 'Alice'"| A
Every web app needs fast data access. When someone visits your e-commerce site:
cart:user123
)session:abc123
)product:789
)Redis solves this problem. Today, we’ll build our own version!
🦀 New Rust Concepts
📚 Database Knowledge
Before You Start
Required:
Time Needed: ~30 minutes
No Prior Rust Knowledge Required! 🎉
Let’s create our workspace:
# Create a new Rust projectcargo new --lib tutorial-01-kv-storecd tutorial-01-kv-store
# Open in your editorcode . # or your preferred editor
Let’s start with the simplest possible key-value store:
// In src/lib.rs// A struct is like a class in other languagespub struct KeyValueStore { // For now, we'll add fields in the next step}
pub struct KeyValueStore {//│ │ └─ The name of our type//│ └─ Keyword to define a new type//└─ "public" - other code can use this}
// In JavaScript, you might write:class KeyValueStore { // fields go here}
// Or with modern JS:const KeyValueStore = class { // implementation};
Key differences:
struct
instead of class
pub
makes it public (like export
)# In Python, you might write:class KeyValueStore: def __init__(self): # Initialize here pass
Key differences:
struct
) from methods (impl
)self
in the struct definitionNow let’s add actual storage to our struct:
use std::collections::HashMap;
pub struct KeyValueStore { // HashMap is like Map in JS or dict in Python data: HashMap<String, String>,}
use std::collections::HashMap;
pub struct KeyValueStore { data: HashMap<String, String>,}
We added:
use
statement to import HashMapdata
that stores String keys and String valuesLet’s add a way to create new key-value stores:
impl KeyValueStore { // This is like a constructor pub fn new() -> Self { KeyValueStore { data: HashMap::new(), } }}
impl KeyValueStore {//│ └─ The type we're adding methods to//└─ "implementation" - where methods live
pub fn new() -> Self { // │ │ └─ Returns our type // │ └─ No parameters // └─ Function name (convention: "new")
KeyValueStore { data: HashMap::new(), } }}
#[cfg(test)]mod tests { use super::*;
#[test] fn new_creates_empty_store() { let store = KeyValueStore::new(); assert!(store.is_empty()); assert_eq!(store.len(), 0); }}
Run it:
cargo test new_creates_empty_store
Now for the core functionality - storing key-value pairs:
impl KeyValueStore { pub fn new() -> Self { KeyValueStore { data: HashMap::new(), } }
// Add this method pub fn set(&mut self, key: String, value: String) { self.data.insert(key, value); }}
pub fn set(&mut self, key: String, value: String) {// │ │// │ └─ "mutable borrow of self"// └─ We need to modify the struct
self.data.insert(key, value); // │ └─ HashMap's insert method // └─ Adds or updates the key}
Let’s add the ability to get values back:
impl KeyValueStore { // ... new() and set() methods ...
pub fn get(&self, key: &str) -> Option<String> { self.data.get(key).cloned() }}
pub fn get(&self, key: &str) -> Option<String> {// │ │ └─ Might return a String// │ └─ Borrow the key (don't take ownership)// └─ Only need to read
self.data.get(key).cloned() // │ └─ Make a copy of the value // └─ Returns Option<&String>}
// JavaScript returns undefined for missing keysconst value = map.get("key"); // undefined or value
// Rust returns Option<T>let value = store.get("key"); // Some(value) or None
Option is Rust’s null-safe way of handling missing values!
Let’s add some helpful methods to check the store’s state:
impl KeyValueStore { // ... previous methods ...
pub fn len(&self) -> usize { self.data.len() }
pub fn is_empty(&self) -> bool { self.data.is_empty() }}
These utility methods follow Rust conventions:
len()
- Returns the number of entries (like Vec, HashMap, etc.)is_empty()
- More readable than len() == 0
Database insight: Real databases track metrics like entry count for performance monitoring!
Let’s put it all together with a comprehensive test:
#[cfg(test)]mod tests { use super::*;
#[test] fn set_stores_value_and_get_retrieves_it() { let mut store = KeyValueStore::new();
// Test basic set and get store.set("user:1".to_string(), "Alice".to_string()); assert_eq!(store.get("user:1"), Some("Alice".to_string()));
// Test missing key assert_eq!(store.get("user:2"), None);
// Test overwrite store.set("user:1".to_string(), "Alice Smith".to_string()); assert_eq!(store.get("user:1"), Some("Alice Smith".to_string())); }}
Run all tests:
cargo test
Let’s see our key-value store in action outside of tests:
use tutorial_01_kv_store::KeyValueStore;
fn main() { let mut store = KeyValueStore::new();
// Store some user data store.set("user:1".to_string(), "Alice".to_string()); store.set("user:2".to_string(), "Bob".to_string());
// Retrieve and display println!("Looking up users...");
match store.get("user:1") { Some(name) => println!("User 1: {}", name), None => println!("User 1 not found"), }
// Try a missing key match store.get("user:999") { Some(name) => println!("User 999: {}", name), None => println!("User 999 not found"), }}
Run it:
cargo run
You should see:
Looking up users...User 1: AliceUser 999 not found
Rust has a helpful tool called clippy
that suggests improvements. Let’s follow one of its recommendations:
use std::collections::HashMap;
#[derive(Default)] // Add this linepub struct KeyValueStore { data: HashMap<String, String>,}
This automatically implements the Default
trait, which provides a standard way to create an empty store.
// With Default, users can now do:let store = KeyValueStore::default();
// In addition to:let store = KeyValueStore::new();
It’s a Rust convention: if your type has an obvious “empty” or “default” state, implement Default!
Bonus: This satisfies Rust’s linter (clippy) and makes your code more idiomatic.
use std::collections::HashMap;
#[derive(Default)]pub struct KeyValueStore { data: HashMap<String, String>,}
impl KeyValueStore { pub fn new() -> Self { KeyValueStore { data: HashMap::new(), } }
pub fn set(&mut self, key: String, value: String) { self.data.insert(key, value); }
pub fn get(&self, key: &str) -> Option<String> { self.data.get(key).cloned() }
pub fn len(&self) -> usize { self.data.len() }
pub fn is_empty(&self) -> bool { self.data.is_empty() }}
// Simplified from ferrisdb-storage/src/memtable/mod.rspub struct MemTable { skiplist: Arc<SkipList>, memory_usage: AtomicUsize, max_size: usize,}
impl MemTable { pub fn set(&self, key: Vec<u8>, value: Vec<u8>) -> Result<()> { // Uses bytes for flexibility // Tracks memory usage // Returns Result for error handling // Uses skip list for ordering }}
Real FerrisDB adds:
We’ll build toward these features in future tutorials!
You’ve successfully built your first database component in Rust!
&mut self
vs &self
impl
blocksYou Found Our Secret! 🤫
Tutorial 2 is still in stealth mode. We’re adding the final touches! Drop us a star if you want us to hurry up! ⭐
Practice Challenges
delete()
method 2. Add a len()
method to count entries 3. Make keys
case-insensitivecargo new --lib tutorial-01-kv-storecargo testcargo test test_specific_name
// Creating a structpub struct Name { field: Type }
// Adding methodsimpl Name { pub fn method(&self) { } // read-only pub fn method(&mut self) { } // read-write}
// Using Optionmatch result { Some(value) => // use value, None => // handle missing,}
Great job! You’ve mastered the basics of Rust and built your first database component! 🚀