| Crates.io | persistent-map |
| lib.rs | persistent-map |
| version | 0.1.3 |
| created_at | 2025-05-15 21:07:33.547775+00 |
| updated_at | 2025-05-15 22:03:35.73088+00 |
| description | An easy-to-use, async, persistent key-value store for Rust, backed by SQLite and designed for extensibility with other storage backends. |
| homepage | |
| repository | https://github.com/ss-sonic/persistent-map |
| max_upload_size | |
| id | 1675697 |
| size | 89,996 |
Persistent-Map offers a simple and efficient way to store and retrieve key-value data that needs to survive application restarts. It provides an easy-to-use API with an asynchronous interface and pluggable storage backends.
insert and get operations with an async interface.async/await for non-blocking operations.DashMap for fast concurrent access to frequently used data.Serialize and DeserializeOwned.Add this to your Cargo.toml:
[dependencies]
persistent-map = "0.1.0" # Replace with the latest version
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # For the async runtime
# Choose the backends you need
persistent-map = { version = "0.1.0", features = ["sqlite", "csv_backend", "in_memory"] }
By default, the crate includes the SQLite and in-memory backends. You can enable other backends as needed.
Here's a simple example using the SQLite backend:
use persistent_map::{PersistentMap, sqlite::SqliteBackend, Result};
#[tokio::main]
async fn main() -> Result<()> {
// Create a SQLite backend
let backend = SqliteBackend::new("my_app_data.db").await?;
// Initialize the map with the backend
let map = PersistentMap::new(backend).await?;
// Insert some data (persists automatically)
map.insert("greeting".to_string(), "Hello, Persistent World!".to_string()).await?;
map.insert("count".to_string(), "42".to_string()).await?;
// Retrieve data (from in-memory cache)
if let Some(value) = map.get(&"greeting".to_string()) {
println!("Greeting: {}", value);
}
// Check if a key exists
if map.contains_key(&"count".to_string()) {
println!("Count exists!");
}
// Remove a key-value pair
let old_value = map.remove(&"greeting".to_string()).await?;
println!("Removed value: {:?}", old_value);
// Ensure all data is persisted
map.flush().await?;
println!("Data persisted. Map contains {} entries.", map.len());
Ok(())
}
The SQLite backend is ideal for most applications, providing reliable persistence with good performance.
use persistent_map::{PersistentMap, sqlite::SqliteBackend, Result};
async fn example() -> Result<()> {
let backend = SqliteBackend::new("my_database.db").await?;
let map = PersistentMap::new(backend).await?;
// Use the map...
Ok(())
}
The CSV backend stores data in a simple CSV file, which can be useful for data that needs to be human-readable.
use persistent_map::{PersistentMap, csv::CsvBackend, Result};
async fn example() -> Result<()> {
let backend = CsvBackend::new("my_data.csv");
let map = PersistentMap::new(backend).await?;
// Use the map...
Ok(())
}
The in-memory backend doesn't provide persistence but can be useful for testing or temporary storage.
use persistent_map::{PersistentMap, in_memory::InMemoryBackend, Result};
async fn example() -> Result<()> {
let backend = InMemoryBackend::new();
let map = PersistentMap::new(backend).await?;
// Use the map...
Ok(())
}
One of the key features of persistent-map is its extensibility. You can create your own storage backends by implementing the StorageBackend trait.
The StorageBackend trait defines the interface that all storage backends must implement:
#[async_trait::async_trait]
pub trait StorageBackend<K, V>
where
K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
{
// Required methods
async fn load_all(&self) -> Result<HashMap<K, V>, PersistentError>;
async fn save(&self, key: K, value: V) -> Result<(), PersistentError>;
async fn delete(&self, key: &K) -> Result<(), PersistentError>;
// Optional methods with default implementations
async fn flush(&self) -> Result<(), PersistentError> { Ok(()) }
async fn contains_key(&self, key: &K) -> Result<bool, PersistentError>;
async fn len(&self) -> Result<usize, PersistentError>;
async fn is_empty(&self) -> Result<bool, PersistentError>;
}
Here's an example of a custom backend that stores data in a JSON file:
use persistent_map::{StorageBackend, PersistentError, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use std::fs;
use serde::{Serialize, de::DeserializeOwned};
use std::hash::Hash;
struct JsonFileBackend {
path: PathBuf,
}
impl JsonFileBackend {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
// Helper method to ensure the file exists
fn ensure_file_exists(&self) -> std::io::Result<()> {
if !self.path.exists() {
// Create parent directories if they don't exist
if let Some(parent) = self.path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
// Create the file with an empty JSON object
fs::write(&self.path, "{}")?;
}
Ok(())
}
}
#[async_trait::async_trait]
impl<K, V> StorageBackend<K, V> for JsonFileBackend
where
K: Eq + Hash + Clone + Serialize + DeserializeOwned + Send + Sync + ToString + 'static,
V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
{
async fn load_all(&self) -> Result<HashMap<K, V>, PersistentError> {
// Ensure the file exists
self.ensure_file_exists()?;
// If the file is empty or contains just "{}", return an empty HashMap
let content = fs::read_to_string(&self.path)?;
if content.trim() == "{}" {
return Ok(HashMap::new());
}
// Parse the JSON file
let map = serde_json::from_str(&content)
.map_err(|e| PersistentError::Serde(e))?;
Ok(map)
}
async fn save(&self, key: K, value: V) -> Result<(), PersistentError> {
// Ensure the file exists
self.ensure_file_exists()?;
// Load existing data
let mut map = self.load_all().await?;
// Update the map
map.insert(key, value);
// Write back to the file
let content = serde_json::to_string_pretty(&map)
.map_err(|e| PersistentError::Serde(e))?;
fs::write(&self.path, content)?;
Ok(())
}
async fn delete(&self, key: &K) -> Result<(), PersistentError> {
// Ensure the file exists
self.ensure_file_exists()?;
// Load existing data
let mut map = self.load_all().await?;
// Remove the key
map.remove(key);
// Write back to the file
let content = serde_json::to_string_pretty(&map)
.map_err(|e| PersistentError::Serde(e))?;
fs::write(&self.path, content)?;
Ok(())
}
}
When implementing a custom backend, consider the following best practices:
PersistentErrorIf you've created a useful backend implementation, consider publishing it as a separate crate that depends on persistent-map. This allows others to benefit from your work while keeping the core crate lightweight.
For example:
persistent-map-redis = { version = "0.1.0", dependencies = { persistent-map = "0.1.0", redis = "0.21.0" } }
DashMap provides fast concurrent access to dataflush() periodically rather than after every writeContributions are welcome! Here are some ways you can contribute:
Please open an issue or submit a pull request on GitHub.
This project follows Semantic Versioning. The current version is 0.1.0, which means it is still in initial development and the API may change.
This project is licensed under the MIT License - see the LICENSE file for details.