cute_sqlite_kv/lib.rs
1//! This crate provides a very small and simple multi-process
2//! persistent key-value store, using `SQLite` for storage.
3//!
4//! The code is intended to be as simple a wrapper around `SQLite`
5//! (via rusqlite) as possible.
6//!
7//! Two stores are provided:
8//!
9//! - [`KVStore`] maps string keys to string values.
10//! - [`BlobStore`] maps string keys to binary (`Vec<u8>`) values.
11//!
12//! Both are thin aliases for the generic [`Store`], which holds all the
13//! logic; only the value type differs. Keys are always `&str`.
14//!
15//! The store can be used from multiple processes, and also opened
16//! multiple times from the same process. File-backed stores use WAL
17//! mode so reads can proceed during a write (see [`Store::new_from_file`]
18//! for the caveats, notably that WAL does not work over network
19//! filesystems).
20//!
21//! While `SQLite` can be very quick, this key-value store is not
22//! intended for high-performance situations, but when you need
23//! something as simple as possible, but still correct. Please feel
24//! free to take, extend, and modify this code for your own requirements!
25//!
26//! # One value type per database file
27//!
28//! A given database file should be used with a single store type. The
29//! value column happily holds whatever you write (text or binary), but
30//! reading a binary value back as a `String` (or a text value as bytes)
31//! fails -- and, per the panic policy below, that failure is a panic.
32//! So pick [`KVStore`] or [`BlobStore`] for a file and stick to it.
33//!
34//! # Errors and panics
35//!
36//! Opening a store (`new_from_file` / `new_in_memory`) returns a
37//! `Result`, because a bad path or permissions is a normal thing a
38//! caller might want to handle.
39//!
40//! Every other operation panics if the underlying `SQLite` call fails.
41//! The reasoning: once the store is open, the only remaining failures
42//! are catastrophic (disk full, corruption, the file vanished) and
43//! there is no sensible recovery. A loud panic is better than a
44//! silently dropped error. Lock contention between processes does
45//! *not* cause a panic: a `busy_timeout` is set so writers wait for
46//! the lock rather than failing.
47//!
48//! # Examples
49//!
50//! ```rust
51//! use cute_sqlite_kv::KVStore;
52//!
53//! let kvstore = KVStore::new_in_memory().unwrap();
54//!
55//! kvstore.insert("key", "value");
56//!
57//! assert_eq!(kvstore.get("key"), Some("value".to_string()));
58//!
59//! kvstore.remove("key");
60//!
61//! assert_eq!(kvstore.get("key"), None);
62//! ```
63//!
64//! Storing binary values with [`BlobStore`]:
65//!
66//! ```rust
67//! use cute_sqlite_kv::BlobStore;
68//!
69//! let store = BlobStore::new_in_memory().unwrap();
70//!
71//! let bytes: &[u8] = &[0, 1, 2, 255];
72//! store.insert("key", bytes);
73//!
74//! assert_eq!(store.get("key"), Some(bytes.to_vec()));
75//! ```
76//!
77//! Creating a file-backed store:
78//!
79//! ```no_run
80//! use cute_sqlite_kv::KVStore;
81//!
82//! let kvstore = KVStore::new_from_file("mydata.db").unwrap();
83//! ```
84use std::marker::PhantomData;
85use std::path::Path;
86use std::time::{Duration, Instant};
87
88use rusqlite::types::FromSql;
89use rusqlite::{Connection, OptionalExtension, ToSql};
90
91const KEY_COLUMN: &str = "KVStore_key";
92const VAL_COLUMN: &str = "KVStore_val";
93const TABLE: &str = "KVStore_table";
94
95/// How long a connection waits for a database lock held by another
96/// connection or process before giving up (and panicking).
97///
98/// Every operation here is a single autocommit statement, so two
99/// writers simply serialise: the loser's busy-handler sleeps and retries
100/// until the winner commits (typically microseconds). This timeout
101/// therefore only matters when some *other* process holds the write lock
102/// for a long time -- a long external transaction, or a hung/crashed
103/// process leaving a stale lock. A genuine deadlock is not affected by
104/// this value: `SQLite` returns `SQLITE_BUSY` immediately in that case
105/// rather than waiting. We pick a generous timeout so a slow but live
106/// lock-holder is tolerated, and only give up (panic) once a wait this
107/// long suggests the holder is never going to release.
108const BUSY_TIMEOUT: Duration = Duration::from_secs(30);
109
110/// Puts a connection's database into WAL mode, robustly under concurrent
111/// cold opens.
112///
113/// Switching a fresh database to WAL needs a brief exclusive lock, and
114/// `SQLite` does *not* apply the connection's `busy_timeout` to a
115/// journal-mode change. So when many processes open the same brand-new
116/// file at once (e.g. lots of instances starting after a reboot), some of
117/// the WAL switches get `SQLITE_BUSY` and would otherwise fail the open.
118/// We retry within the same time budget as `busy_timeout`.
119///
120/// WAL is persistent in the database header, so the common warm-open case
121/// (already WAL) takes the fast path and never contends for the lock.
122fn enable_wal(connection: &Connection) -> rusqlite::Result<()> {
123 let current: String = connection.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
124 if current.eq_ignore_ascii_case("wal") {
125 return Ok(());
126 }
127
128 let deadline = Instant::now() + BUSY_TIMEOUT;
129 let mut backoff = Duration::from_millis(1);
130 loop {
131 match connection.query_row("PRAGMA journal_mode=WAL", [], |row| row.get::<_, String>(0)) {
132 // The resulting mode may not be "wal" on a filesystem that does
133 // not support it (e.g. NFS); that is still a correct store, just
134 // without WAL, so we accept whatever we got.
135 Ok(_) => return Ok(()),
136 Err(e) if is_locked(&e) && Instant::now() < deadline => {
137 std::thread::sleep(backoff);
138 backoff = (backoff * 2).min(Duration::from_millis(50));
139 }
140 Err(e) => return Err(e),
141 }
142 }
143}
144
145/// Whether an error is a `SQLITE_BUSY`/`SQLITE_LOCKED` lock-contention error.
146fn is_locked(error: &rusqlite::Error) -> bool {
147 matches!(
148 error,
149 rusqlite::Error::SqliteFailure(e, _)
150 if matches!(
151 e.code,
152 rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
153 )
154 )
155}
156
157/// A type that can be used as the value of a [`Store`].
158///
159/// This ties the owned value type (what [`Store::get`] returns) to its
160/// borrowed form (what [`Store::insert`] accepts). It is implemented for
161/// `String` (borrowed as `str`) and `Vec<u8>` (borrowed as `[u8]`).
162pub trait StoreValue: FromSql {
163 /// The borrowed form accepted by [`Store::insert`]: `str` for
164 /// `String`, `[u8]` for `Vec<u8>`.
165 type Ref: ToSql + ?Sized;
166}
167
168impl StoreValue for String {
169 type Ref = str;
170}
171
172impl StoreValue for Vec<u8> {
173 type Ref = [u8];
174}
175
176/// A simple key-value store backed by `SQLite`, generic over its value
177/// type.
178///
179/// You will normally use one of the aliases [`KVStore`] (string values)
180/// or [`BlobStore`] (binary values) rather than naming `Store` directly.
181pub struct Store<V> {
182 connection: Connection,
183 _marker: PhantomData<fn() -> V>,
184}
185
186/// A key-value store mapping string keys to string values.
187pub type KVStore = Store<String>;
188
189/// A key-value store mapping string keys to binary (`Vec<u8>`) values.
190pub type BlobStore = Store<Vec<u8>>;
191
192impl<V: StoreValue> Store<V> {
193 /// Creates a new in-memory key-value store.
194 ///
195 /// An in-memory key-value store is in practice worse than
196 /// a standard `HashMap` in every way, so the only use of this function
197 /// is for creating a key value store for testing.
198 ///
199 /// # Examples
200 ///
201 /// ```rust
202 /// use cute_sqlite_kv::KVStore;
203 ///
204 /// let kvstore = KVStore::new_in_memory().unwrap();
205 /// ```
206 pub fn new_in_memory() -> rusqlite::Result<Store<V>> {
207 let connection = Connection::open_in_memory()?;
208 connection.busy_timeout(BUSY_TIMEOUT)?;
209 let store = Store {
210 connection,
211 _marker: PhantomData,
212 };
213 store.create_table()?;
214 Ok(store)
215 }
216
217 /// Creates a new store using a file as the storage.
218 ///
219 /// The database is put into WAL (write-ahead logging) mode, so that
220 /// readers can proceed while another connection is writing. This is
221 /// persistent and creates two sidecar files next to the database
222 /// (`<file>-wal` and `<file>-shm`). WAL requires all accessing
223 /// processes to be on the same machine as the file; it is not
224 /// supported on network filesystems such as NFS.
225 ///
226 /// # Arguments
227 ///
228 /// * `filename` - The path to the file used as the storage for the store.
229 ///
230 /// # Examples
231 ///
232 /// ```no_run
233 /// use cute_sqlite_kv::KVStore;
234 ///
235 /// let kvstore = KVStore::new_from_file("mydata.db").unwrap();
236 /// ```
237 pub fn new_from_file(filename: impl AsRef<Path>) -> rusqlite::Result<Store<V>> {
238 let connection = Connection::open(filename)?;
239 connection.busy_timeout(BUSY_TIMEOUT)?;
240 // Enable WAL so reads don't block on a concurrent write. This is a
241 // no-op for in-memory databases, which is why it lives here rather
242 // than in `new_in_memory`.
243 enable_wal(&connection)?;
244 let store = Store {
245 connection,
246 _marker: PhantomData,
247 };
248 store.create_table()?;
249 Ok(store)
250 }
251
252 /// Internal function which ensures the store's table is created
253 fn create_table(&self) -> rusqlite::Result<()> {
254 self.connection.execute(
255 &format!(
256 "CREATE TABLE IF NOT EXISTS {TABLE} (
257 {KEY_COLUMN} varchar PRIMARY KEY UNIQUE NOT NULL,
258 {VAL_COLUMN}
259 )"
260 ),
261 (),
262 )?;
263 Ok(())
264 }
265
266 /// Inserts a key-value pair in the store.
267 /// Overwrites any existing value.
268 ///
269 /// # Arguments
270 ///
271 /// * `key` - The key for the value.
272 /// * `value` - The value to be stored.
273 ///
274 /// # Panics
275 ///
276 /// Panics if the underlying `SQLite` write fails.
277 ///
278 /// # Examples
279 ///
280 /// ```rust
281 /// use cute_sqlite_kv::KVStore;
282 ///
283 /// let kvstore = KVStore::new_in_memory().unwrap();
284 ///
285 /// kvstore.insert("key", "value");
286 /// ```
287 pub fn insert(&self, key: &str, value: &V::Ref) {
288 self.connection
289 .execute(
290 &format!("REPLACE INTO {TABLE} ({KEY_COLUMN}, {VAL_COLUMN}) VALUES (?, ?)"),
291 rusqlite::params![key, value],
292 )
293 .expect("cute-sqlite-kv: insert failed");
294 }
295
296 /// Checks if a particular key is contained in the store.
297 ///
298 /// # Arguments
299 ///
300 /// * `key` - The key to check for existence.
301 ///
302 /// # Panics
303 ///
304 /// Panics if the underlying `SQLite` query fails.
305 ///
306 /// # Examples
307 ///
308 /// ```rust
309 /// use cute_sqlite_kv::KVStore;
310 ///
311 /// let kvstore = KVStore::new_in_memory().unwrap();
312 ///
313 /// kvstore.insert("key", "value");
314 ///
315 /// assert!(kvstore.contains_key("key"));
316 /// assert!(!kvstore.contains_key("nonexistent_key"));
317 /// ```
318 pub fn contains_key(&self, key: &str) -> bool {
319 let exists: i64 = self
320 .connection
321 .query_row(
322 &format!("SELECT EXISTS(SELECT 1 FROM {TABLE} WHERE {KEY_COLUMN} = ?)"),
323 [key],
324 |row| row.get(0),
325 )
326 .expect("cute-sqlite-kv: contains_key query failed");
327 exists != 0
328 }
329
330 /// Retrieves the value for a given key from the store.
331 ///
332 /// # Arguments
333 ///
334 /// * `key` - The key to retrieve the value for.
335 ///
336 /// # Panics
337 ///
338 /// Panics if the underlying `SQLite` query fails.
339 ///
340 /// # Examples
341 ///
342 /// ```rust
343 /// use cute_sqlite_kv::KVStore;
344 ///
345 /// let kvstore = KVStore::new_in_memory().unwrap();
346 ///
347 /// kvstore.insert("key", "value");
348 ///
349 /// assert_eq!(kvstore.get("key"), Some("value".to_string()));
350 /// ```
351 pub fn get(&self, key: &str) -> Option<V> {
352 self.connection
353 .query_row(
354 &format!("SELECT {VAL_COLUMN} FROM {TABLE} WHERE {KEY_COLUMN} = ?"),
355 [key],
356 |row| row.get(0),
357 )
358 .optional()
359 .expect("cute-sqlite-kv: get query failed")
360 }
361
362 /// Removes a key-value pair from the store,
363 /// if present, and returns the old value if it existed.
364 ///
365 /// # Arguments
366 ///
367 /// * `key` - The key to remove.
368 ///
369 /// # Panics
370 ///
371 /// Panics if the underlying `SQLite` write fails.
372 ///
373 /// # Examples
374 ///
375 /// ```rust
376 /// use cute_sqlite_kv::KVStore;
377 ///
378 /// let kvstore = KVStore::new_in_memory().unwrap();
379 ///
380 /// kvstore.insert("key", "value");
381 ///
382 /// assert_eq!(kvstore.remove("key"), Some("value".to_string()));
383 ///
384 /// assert_eq!(kvstore.get("key"), None);
385 ///
386 /// assert_eq!(kvstore.remove("key"), None);
387 /// ```
388 pub fn remove(&self, key: &str) -> Option<V> {
389 self.connection
390 .query_row(
391 &format!("DELETE FROM {TABLE} WHERE {KEY_COLUMN} = ? RETURNING {VAL_COLUMN}"),
392 [key],
393 |row| row.get(0),
394 )
395 .optional()
396 .expect("cute-sqlite-kv: remove failed")
397 }
398
399 /// Clears the entire table in the store.
400 ///
401 /// This method removes all key-value pairs from the table, effectively clearing the entire store.
402 ///
403 /// # Panics
404 ///
405 /// Panics if the underlying `SQLite` write fails.
406 ///
407 /// # Examples
408 ///
409 /// ```rust
410 /// use cute_sqlite_kv::KVStore;
411 ///
412 /// let kvstore = KVStore::new_in_memory().unwrap();
413 ///
414 /// kvstore.insert("key1", "value1");
415 /// kvstore.insert("key2", "value2");
416 ///
417 /// kvstore.clear();
418 ///
419 /// assert_eq!(kvstore.get("key1"), None);
420 /// assert_eq!(kvstore.get("key2"), None);
421 /// ```
422 pub fn clear(&self) {
423 self.connection
424 .execute(&format!("DELETE FROM {TABLE}"), ())
425 .expect("cute-sqlite-kv: clear failed");
426 }
427
428 /// Checks if the store is empty.
429 ///
430 /// Note: Since the store can be used concurrently, the result of this method
431 /// can be out of date almost immediately.
432 ///
433 /// # Panics
434 ///
435 /// Panics if the underlying `SQLite` query fails.
436 ///
437 /// # Examples
438 ///
439 /// ```rust
440 /// use cute_sqlite_kv::KVStore;
441 ///
442 /// let kvstore = KVStore::new_in_memory().unwrap();
443 /// assert!(kvstore.is_empty());
444 ///
445 /// kvstore.insert("key", "value");
446 /// assert!(!kvstore.is_empty());
447 /// ```
448 pub fn is_empty(&self) -> bool {
449 let empty: i64 = self
450 .connection
451 .query_row(
452 &format!("SELECT NOT EXISTS(SELECT 1 FROM {TABLE})"),
453 [],
454 |row| row.get(0),
455 )
456 .expect("cute-sqlite-kv: is_empty query failed");
457 empty != 0
458 }
459
460 /// Returns the number of key-value pairs in the store.
461 ///
462 /// Note: Since the store can be used concurrently, the result of this method
463 /// can be out of date almost immediately.
464 ///
465 /// # Panics
466 ///
467 /// Panics if the underlying `SQLite` query fails.
468 ///
469 /// # Examples
470 ///
471 /// ```rust
472 /// use cute_sqlite_kv::KVStore;
473 ///
474 /// let kvstore = KVStore::new_in_memory().unwrap();
475 /// assert_eq!(kvstore.len(), 0);
476 ///
477 /// kvstore.insert("key1", "value1");
478 /// kvstore.insert("key2", "value2");
479 /// assert_eq!(kvstore.len(), 2);
480 /// ```
481 pub fn len(&self) -> usize {
482 let count: i64 = self
483 .connection
484 .query_row(&format!("SELECT COUNT(*) FROM {TABLE}"), [], |row| {
485 row.get(0)
486 })
487 .expect("cute-sqlite-kv: len query failed");
488 count as usize
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 use tempfile::tempdir;
497
498 #[test]
499 fn test_new_in_memory() {
500 let _ = KVStore::new_in_memory().unwrap();
501 }
502
503 #[test]
504 fn test_new_from_file() {
505 let temp_dir = tempdir().expect("Failed to create temp directory");
506 let filename = temp_dir.path().join("kvstore.db");
507 let _ = KVStore::new_from_file(&filename).unwrap();
508 }
509
510 #[test]
511 fn test_new_from_file_more() {
512 let temp_dir = tempdir().expect("Failed to create temp directory");
513 let filename = temp_dir.path().join("kvstore.db");
514 let kvstore = KVStore::new_from_file(&filename).unwrap();
515 let key = "test_key";
516 let value = "test_value";
517 kvstore.insert(key, value);
518 let result = kvstore.get(key);
519 assert_eq!(result, Some(value.to_string()));
520 }
521
522 #[test]
523 fn test_reopen_database() {
524 let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
525 let filename = temp_dir.path().join("kvstore.db");
526 {
527 let kvstore = KVStore::new_from_file(&filename).unwrap();
528 let key = "test_key";
529 let value = "test_value";
530 kvstore.insert(key, value);
531 }
532 {
533 let kvstore = KVStore::new_from_file(&filename).unwrap();
534 let key = "test_key";
535 let result = kvstore.get(key);
536 assert_eq!(result, Some("test_value".to_string()));
537 }
538 }
539
540 #[test]
541 fn test_insert_and_get() {
542 let kvstore = KVStore::new_in_memory().unwrap();
543 let key = "test_key";
544 let value = "test_value";
545 kvstore.insert(key, value);
546 let result = kvstore.get(key);
547 assert_eq!(result, Some(value.to_string()));
548 }
549
550 #[test]
551 fn test_get_nonexistent_key() {
552 let kvstore = KVStore::new_in_memory().unwrap();
553 let key = "nonexistent_key";
554 let result = kvstore.get(key);
555 assert_eq!(result, None);
556 }
557
558 #[test]
559 fn test_remove() {
560 let kvstore = KVStore::new_in_memory().unwrap();
561 let key = "test_key";
562 let value = "test_value";
563 kvstore.insert(key, value);
564 let old_value = kvstore.remove(key);
565 assert_eq!(old_value, Some(value.to_string()));
566 let result = kvstore.get(key);
567 assert_eq!(result, None);
568 }
569
570 #[test]
571 fn test_remove_nonexistent_key() {
572 let kvstore = KVStore::new_in_memory().unwrap();
573 let key = "nonexistent_key";
574 let old_value = kvstore.remove(key);
575 assert_eq!(old_value, None);
576 let result = kvstore.get(key);
577 assert_eq!(result, None);
578 }
579
580 #[test]
581 fn test_clear() {
582 let kvstore = KVStore::new_in_memory().unwrap();
583 let key = "test_key";
584 let value = "test_value";
585 kvstore.insert(key, value);
586 kvstore.clear();
587 let result = kvstore.get(key);
588 assert_eq!(result, None);
589 }
590
591 #[test]
592 fn test_many_connections() {
593 let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
594 let filename = temp_dir.path().join("kvstore.db");
595
596 // Create the first connection and add a key
597 {
598 let kvstore = KVStore::new_from_file(&filename).unwrap();
599 let key = "test_key";
600 let value = "test_value";
601 kvstore.insert(key, value);
602 }
603
604 // Check if the key is there
605 {
606 let kvstore = KVStore::new_from_file(&filename).unwrap();
607 let key = "test_key";
608 let result = kvstore.get(key);
609 assert_eq!(result, Some("test_value".to_string()));
610 }
611
612 // remove the key
613 {
614 let kvstore = KVStore::new_from_file(&filename).unwrap();
615 let key = "test_key";
616 kvstore.remove(key);
617 }
618
619 // Check if the key is gone
620 {
621 let kvstore = KVStore::new_from_file(&filename).unwrap();
622 let key = "test_key";
623 let result = kvstore.get(key);
624 assert_eq!(result, None);
625 }
626 }
627
628 #[test]
629 fn test_overlapping_connections() {
630 let temp_dir = tempfile::tempdir().expect("Failed to create temp directory");
631 let filename = temp_dir.path().join("kvstore.db");
632
633 let kvstore = KVStore::new_from_file(&filename).unwrap();
634
635 // Create the first connection and add a key
636 {
637 let key = "test_key";
638 let value = "test_value";
639 kvstore.insert(key, value);
640 }
641
642 let kvstore2 = KVStore::new_from_file(&filename).unwrap();
643
644 // Check if the key is there
645 {
646 let key = "test_key";
647 let result = kvstore2.get(key);
648 assert_eq!(result, Some("test_value".to_string()));
649 }
650
651 // remove the key
652 {
653 let key = "test_key";
654 kvstore2.remove(key);
655 }
656
657 // Check if the key is gone
658 {
659 let key = "test_key";
660 let result = kvstore.get(key);
661 assert_eq!(result, None);
662 }
663 }
664
665 #[test]
666 fn test_insert_multiple_times() {
667 let kvstore = KVStore::new_in_memory().unwrap();
668 let key = "test_key";
669 let value1 = "test_value1";
670 let value2 = "test_value2";
671 let value3 = "test_value3";
672
673 kvstore.insert(key, value1);
674 let result1 = kvstore.get(key);
675 assert_eq!(result1, Some(value1.to_string()));
676
677 kvstore.insert(key, value2);
678 let result2 = kvstore.get(key);
679 assert_eq!(result2, Some(value2.to_string()));
680
681 kvstore.insert(key, value3);
682 let result3 = kvstore.get(key);
683 assert_eq!(result3, Some(value3.to_string()));
684 }
685
686 #[test]
687 fn test_blob_roundtrip() {
688 let store = BlobStore::new_in_memory().unwrap();
689 // Deliberately not valid UTF-8.
690 let value: &[u8] = &[0u8, 159, 146, 150, 255];
691 store.insert("key", value);
692 assert_eq!(store.get("key"), Some(value.to_vec()));
693 assert_eq!(store.remove("key"), Some(value.to_vec()));
694 assert_eq!(store.get("key"), None);
695 }
696
697 #[test]
698 fn test_file_uses_wal() {
699 let temp_dir = tempdir().expect("Failed to create temp directory");
700 let filename = temp_dir.path().join("kvstore.db");
701 let _store = KVStore::new_from_file(&filename).unwrap();
702
703 // WAL mode is persistent in the database header, so a fresh
704 // connection reports it.
705 let raw = rusqlite::Connection::open(&filename).unwrap();
706 let mode: String = raw
707 .query_row("PRAGMA journal_mode", [], |row| row.get(0))
708 .unwrap();
709 assert_eq!(mode, "wal");
710 }
711
712 #[test]
713 fn test_blob_reopen() {
714 let temp_dir = tempdir().expect("Failed to create temp directory");
715 let filename = temp_dir.path().join("blobstore.db");
716 let value: &[u8] = &[10, 20, 0, 255, 128];
717 {
718 let store = BlobStore::new_from_file(&filename).unwrap();
719 store.insert("key", value);
720 }
721 {
722 let store = BlobStore::new_from_file(&filename).unwrap();
723 assert_eq!(store.get("key"), Some(value.to_vec()));
724 }
725 }
726}