use std::sync::Mutex;
use rusqlite::Connection;
use crate::error::{KernelError, Result};
pub trait KvStore: Send + Sync {
fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
fn put(&self, key: &str, value: &[u8]) -> Result<()>;
fn delete(&self, key: &str) -> Result<bool>;
}
const KV_DDL: &str = "CREATE TABLE IF NOT EXISTS kv (
key TEXT PRIMARY KEY,
value BLOB NOT NULL
);";
pub struct SqliteKvStore {
conn: Mutex<Connection>,
}
impl SqliteKvStore {
pub fn open(path: &std::path::Path) -> Result<Self> {
let conn = Connection::open(path).map_err(|e| KernelError::Store(e.to_string()))?;
Self::init(&conn)?;
Ok(Self {
conn: Mutex::new(conn),
})
}
pub fn open_in_memory() -> Result<Self> {
let conn = Connection::open_in_memory().map_err(|e| KernelError::Store(e.to_string()))?;
Self::init(&conn)?;
Ok(Self {
conn: Mutex::new(conn),
})
}
fn init(conn: &Connection) -> Result<()> {
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;")
.map_err(|e| KernelError::Store(e.to_string()))?;
conn.execute_batch(KV_DDL)
.map_err(|e| KernelError::Store(e.to_string()))?;
Ok(())
}
fn lock(&self) -> std::sync::MutexGuard<'_, Connection> {
self.conn.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl KvStore for SqliteKvStore {
fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let conn = self.lock();
match conn.query_row(
"SELECT value FROM kv WHERE key = ?1",
rusqlite::params![key],
|r| r.get::<_, Vec<u8>>(0),
) {
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(KernelError::Store(e.to_string())),
}
}
fn put(&self, key: &str, value: &[u8]) -> Result<()> {
let conn = self.lock();
conn.execute(
"INSERT OR REPLACE INTO kv (key, value) VALUES (?1, ?2)",
rusqlite::params![key, value],
)
.map_err(|e| KernelError::Store(e.to_string()))?;
Ok(())
}
fn delete(&self, key: &str) -> Result<bool> {
let conn = self.lock();
let n = conn
.execute("DELETE FROM kv WHERE key = ?1", rusqlite::params![key])
.map_err(|e| KernelError::Store(e.to_string()))?;
Ok(n > 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_in_memory() {
let kv = SqliteKvStore::open_in_memory().unwrap();
assert!(kv.get("missing").unwrap().is_none());
kv.put("a", b"value-a").unwrap();
assert_eq!(kv.get("a").unwrap(), Some(b"value-a".to_vec()));
kv.put("a", b"value-a2").unwrap();
assert_eq!(kv.get("a").unwrap(), Some(b"value-a2".to_vec()));
assert!(kv.delete("a").unwrap());
assert!(kv.get("a").unwrap().is_none());
assert!(!kv.delete("a").unwrap());
}
#[test]
fn open_on_file_persists() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("kv.db");
{
let kv = SqliteKvStore::open(&path).unwrap();
kv.put("k", b"v").unwrap();
}
let kv = SqliteKvStore::open(&path).unwrap();
assert_eq!(kv.get("k").unwrap(), Some(b"v".to_vec()));
}
#[test]
fn trait_object_round_trip() {
let kv: Box<dyn KvStore> = Box::new(SqliteKvStore::open_in_memory().unwrap());
kv.put("x", b"y").unwrap();
assert_eq!(kv.get("x").unwrap(), Some(b"y".to_vec()));
}
}