use std::marker::PhantomData;
use std::path::Path;
use std::time::{Duration, Instant};
use rusqlite::types::FromSql;
use rusqlite::{Connection, OptionalExtension, ToSql};
const KEY_COLUMN: &str = "KVStore_key";
const VAL_COLUMN: &str = "KVStore_val";
const TABLE: &str = "KVStore_table";
const BUSY_TIMEOUT: Duration = Duration::from_secs(30);
fn enable_wal(connection: &Connection) -> rusqlite::Result<()> {
let current: String = connection.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
if current.eq_ignore_ascii_case("wal") {
return Ok(());
}
let deadline = Instant::now() + BUSY_TIMEOUT;
let mut backoff = Duration::from_millis(1);
loop {
match connection.query_row("PRAGMA journal_mode=WAL", [], |row| row.get::<_, String>(0)) {
Ok(_) => return Ok(()),
Err(e) if is_locked(&e) && Instant::now() < deadline => {
std::thread::sleep(backoff);
backoff = (backoff * 2).min(Duration::from_millis(50));
}
Err(e) => return Err(e),
}
}
}
fn is_locked(error: &rusqlite::Error) -> bool {
matches!(
error,
rusqlite::Error::SqliteFailure(e, _)
if matches!(
e.code,
rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
)
)
}
pub trait StoreValue: FromSql {
type Ref: ToSql + ?Sized;
}
impl StoreValue for String {
type Ref = str;
}
impl StoreValue for Vec<u8> {
type Ref = [u8];
}
pub struct Store<V> {
connection: Connection,
_marker: PhantomData<fn() -> V>,
}
pub type KVStore = Store<String>;
pub type BlobStore = Store<Vec<u8>>;
impl<V: StoreValue> Store<V> {
pub fn new_in_memory() -> rusqlite::Result<Store<V>> {
let connection = Connection::open_in_memory()?;
connection.busy_timeout(BUSY_TIMEOUT)?;
let store = Store {
connection,
_marker: PhantomData,
};
store.create_table()?;
Ok(store)
}
pub fn new_from_file(filename: impl AsRef<Path>) -> rusqlite::Result<Store<V>> {
let connection = Connection::open(filename)?;
connection.busy_timeout(BUSY_TIMEOUT)?;
enable_wal(&connection)?;
let store = Store {
connection,
_marker: PhantomData,
};
store.create_table()?;
Ok(store)
}
fn create_table(&self) -> rusqlite::Result<()> {
self.connection.execute(
&format!(
"CREATE TABLE IF NOT EXISTS {TABLE} (
{KEY_COLUMN} varchar PRIMARY KEY UNIQUE NOT NULL,
{VAL_COLUMN}
)"
),
(),
)?;
Ok(())
}
pub fn insert(&self, key: &str, value: &V::Ref) {
self.connection
.execute(
&format!("REPLACE INTO {TABLE} ({KEY_COLUMN}, {VAL_COLUMN}) VALUES (?, ?)"),
rusqlite::params![key, value],
)
.expect("cute-sqlite-kv: insert failed");
}
pub fn contains_key(&self, key: &str) -> bool {
let exists: i64 = self
.connection
.query_row(
&format!("SELECT EXISTS(SELECT 1 FROM {TABLE} WHERE {KEY_COLUMN} = ?)"),
[key],
|row| row.get(0),
)
.expect("cute-sqlite-kv: contains_key query failed");
exists != 0
}
pub fn get(&self, key: &str) -> Option<V> {
self.connection
.query_row(
&format!("SELECT {VAL_COLUMN} FROM {TABLE} WHERE {KEY_COLUMN} = ?"),
[key],
|row| row.get(0),
)
.optional()
.expect("cute-sqlite-kv: get query failed")
}
pub fn remove(&self, key: &str) -> Option<V> {
self.connection
.query_row(
&format!("DELETE FROM {TABLE} WHERE {KEY_COLUMN} = ? RETURNING {VAL_COLUMN}"),
[key],
|row| row.get(0),
)
.optional()
.expect("cute-sqlite-kv: remove failed")
}
pub fn clear(&self) {
self.connection
.execute(&format!("DELETE FROM {TABLE}"), ())
.expect("cute-sqlite-kv: clear failed");
}
pub fn is_empty(&self) -> bool {
let empty: i64 = self
.connection
.query_row(
&format!("SELECT NOT EXISTS(SELECT 1 FROM {TABLE})"),
[],
|row| row.get(0),
)
.expect("cute-sqlite-kv: is_empty query failed");
empty != 0
}
pub fn len(&self) -> usize {
let count: i64 = self
.connection
.query_row(&format!("SELECT COUNT(*) FROM {TABLE}"), [], |row| {
row.get(0)
})
.expect("cute-sqlite-kv: len query failed");
count as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_new_in_memory() {
let _ = KVStore::new_in_memory().unwrap();
}
#[test]
fn test_new_from_file() {
let temp_dir = tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("kvstore.db");
let _ = KVStore::new_from_file(&filename).unwrap();
}
#[test]
fn test_new_from_file_more() {
let temp_dir = tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("kvstore.db");
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
let result = kvstore.get(key);
assert_eq!(result, Some(value.to_string()));
}
#[test]
fn test_reopen_database() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("kvstore.db");
{
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
}
{
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
let result = kvstore.get(key);
assert_eq!(result, Some("test_value".to_string()));
}
}
#[test]
fn test_insert_and_get() {
let kvstore = KVStore::new_in_memory().unwrap();
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
let result = kvstore.get(key);
assert_eq!(result, Some(value.to_string()));
}
#[test]
fn test_get_nonexistent_key() {
let kvstore = KVStore::new_in_memory().unwrap();
let key = "nonexistent_key";
let result = kvstore.get(key);
assert_eq!(result, None);
}
#[test]
fn test_remove() {
let kvstore = KVStore::new_in_memory().unwrap();
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
let old_value = kvstore.remove(key);
assert_eq!(old_value, Some(value.to_string()));
let result = kvstore.get(key);
assert_eq!(result, None);
}
#[test]
fn test_remove_nonexistent_key() {
let kvstore = KVStore::new_in_memory().unwrap();
let key = "nonexistent_key";
let old_value = kvstore.remove(key);
assert_eq!(old_value, None);
let result = kvstore.get(key);
assert_eq!(result, None);
}
#[test]
fn test_clear() {
let kvstore = KVStore::new_in_memory().unwrap();
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
kvstore.clear();
let result = kvstore.get(key);
assert_eq!(result, None);
}
#[test]
fn test_many_connections() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("kvstore.db");
{
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
}
{
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
let result = kvstore.get(key);
assert_eq!(result, Some("test_value".to_string()));
}
{
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
kvstore.remove(key);
}
{
let kvstore = KVStore::new_from_file(&filename).unwrap();
let key = "test_key";
let result = kvstore.get(key);
assert_eq!(result, None);
}
}
#[test]
fn test_overlapping_connections() {
let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("kvstore.db");
let kvstore = KVStore::new_from_file(&filename).unwrap();
{
let key = "test_key";
let value = "test_value";
kvstore.insert(key, value);
}
let kvstore2 = KVStore::new_from_file(&filename).unwrap();
{
let key = "test_key";
let result = kvstore2.get(key);
assert_eq!(result, Some("test_value".to_string()));
}
{
let key = "test_key";
kvstore2.remove(key);
}
{
let key = "test_key";
let result = kvstore.get(key);
assert_eq!(result, None);
}
}
#[test]
fn test_insert_multiple_times() {
let kvstore = KVStore::new_in_memory().unwrap();
let key = "test_key";
let value1 = "test_value1";
let value2 = "test_value2";
let value3 = "test_value3";
kvstore.insert(key, value1);
let result1 = kvstore.get(key);
assert_eq!(result1, Some(value1.to_string()));
kvstore.insert(key, value2);
let result2 = kvstore.get(key);
assert_eq!(result2, Some(value2.to_string()));
kvstore.insert(key, value3);
let result3 = kvstore.get(key);
assert_eq!(result3, Some(value3.to_string()));
}
#[test]
fn test_blob_roundtrip() {
let store = BlobStore::new_in_memory().unwrap();
let value: &[u8] = &[0u8, 159, 146, 150, 255];
store.insert("key", value);
assert_eq!(store.get("key"), Some(value.to_vec()));
assert_eq!(store.remove("key"), Some(value.to_vec()));
assert_eq!(store.get("key"), None);
}
#[test]
fn test_file_uses_wal() {
let temp_dir = tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("kvstore.db");
let _store = KVStore::new_from_file(&filename).unwrap();
let raw = rusqlite::Connection::open(&filename).unwrap();
let mode: String = raw
.query_row("PRAGMA journal_mode", [], |row| row.get(0))
.unwrap();
assert_eq!(mode, "wal");
}
#[test]
fn test_blob_reopen() {
let temp_dir = tempdir().expect("Failed to create temp directory");
let filename = temp_dir.path().join("blobstore.db");
let value: &[u8] = &[10, 20, 0, 255, 128];
{
let store = BlobStore::new_from_file(&filename).unwrap();
store.insert("key", value);
}
{
let store = BlobStore::new_from_file(&filename).unwrap();
assert_eq!(store.get("key"), Some(value.to_vec()));
}
}
}