persistent_map/backends/sqlite.rs
1//! `SQLite` backend implementation for `PersistentMap`.
2//!
3//! This module provides a `SQLite`-based storage backend for `PersistentMap`.
4//! It uses `tokio-rusqlite` for asynchronous `SQLite` operations.
5
6use crate::StorageBackend;
7use crate::{PersistentError, Result};
8use serde::{de::DeserializeOwned, Serialize};
9use std::{collections::HashMap, hash::Hash, str::FromStr};
10use tokio_rusqlite::{params, Connection};
11
12/// A `SQLite`-based storage backend for `PersistentMap`.
13///
14/// This backend stores key-value pairs in a `SQLite` database, providing
15/// durable persistence with good performance characteristics.
16///
17/// # Examples
18///
19/// ```rust,no_run
20/// use persistent_map::{PersistentMap, Result};
21/// use persistent_map::sqlite::SqliteBackend;
22///
23/// # async fn example() -> Result<()> {
24/// // Create a SQLite backend
25/// let backend = SqliteBackend::new("my_database.db").await?;
26///
27/// // Initialize a PersistentMap with the backend
28/// let map: PersistentMap<String, String, _> = PersistentMap::new(backend).await?;
29/// # Ok(())
30/// # }
31/// ```
32#[derive(Debug)]
33pub struct SqliteBackend {
34 /// The `SQLite` connection
35 conn: Connection,
36}
37
38impl SqliteBackend {
39 /// Creates a new `SQLite` backend with the given database path.
40 ///
41 /// This method opens a connection to the `SQLite` database at the specified path
42 /// and creates the necessary table if it doesn't exist.
43 ///
44 /// # Arguments
45 ///
46 /// * `db_path` - The path to the `SQLite` database file
47 ///
48 /// # Returns
49 ///
50 /// A `Result` containing the new `SqliteBackend` or an error
51 ///
52 /// # Examples
53 ///
54 /// ```rust,no_run
55 /// use persistent_map::sqlite::SqliteBackend;
56 /// use persistent_map::Result;
57 ///
58 /// # async fn example() -> Result<()> {
59 /// let backend = SqliteBackend::new("my_database.db").await?;
60 /// # Ok(())
61 /// # }
62 /// ```
63 /// # Errors
64 ///
65 /// Returns an error if the database connection cannot be opened or if
66 /// the initial table/index creation fails.
67 pub async fn new(db_path: &str) -> Result<Self> {
68 let conn = Connection::open(db_path).await?;
69 conn.call(|c| {
70 c.execute(
71 "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
72 [],
73 )
74 .map_err(tokio_rusqlite::Error::Rusqlite)
75 })
76 .await?;
77
78 // Create an index for faster lookups if it doesn't exist
79 conn.call(|c| {
80 c.execute("CREATE INDEX IF NOT EXISTS kv_key_idx ON kv (key)", [])
81 .map_err(tokio_rusqlite::Error::Rusqlite)
82 })
83 .await?;
84
85 Ok(Self { conn })
86 }
87
88 /// Returns the path to the `SQLite` database file.
89 ///
90 /// # Returns
91 ///
92 /// A `Result` containing the path to the database file or an error
93 ///
94 /// # Errors
95 ///
96 /// Returns an error if the `PRAGMA database_list` query fails or if the path
97 /// cannot be retrieved from the query result.
98 pub async fn db_path(&self) -> Result<String> {
99 let result = self
100 .conn
101 .call(|c| {
102 c.query_row("PRAGMA database_list", [], |row| {
103 let path: String = row.get(2)?;
104 Ok(path)
105 })
106 .map_err(tokio_rusqlite::Error::Rusqlite)
107 })
108 .await?; // Use ? to convert the error type
109
110 Ok(result)
111 }
112}
113
114/// Implementation of the `StorageBackend` trait for `SqliteBackend`.
115///
116/// This implementation provides methods for loading, saving, and deleting
117/// key-value pairs from a SQLite database.
118#[async_trait::async_trait]
119impl<K, V> StorageBackend<K, V> for SqliteBackend
120where
121 K: Eq
122 + Hash
123 + Clone
124 + Serialize
125 + DeserializeOwned
126 + Send
127 + Sync
128 + 'static
129 + ToString
130 + FromStr,
131 <K as FromStr>::Err: std::error::Error + Send + Sync + 'static,
132 V: Clone + Serialize + DeserializeOwned + Send + Sync + 'static,
133{
134 /// Loads all key-value pairs from the SQLite database.
135 ///
136 /// This method queries the database for all key-value pairs and deserializes
137 /// them into the appropriate types.
138 async fn load_all(&self) -> Result<HashMap<K, V>, PersistentError> {
139 let rows = self
140 .conn
141 .call(|c| {
142 let mut stmt = c.prepare_cached("SELECT key, value FROM kv")?;
143 let mut map = HashMap::with_capacity(100); // Pre-allocate for better performance
144 let mut rows_iter = stmt.query_map([], |r| {
145 let key_str: String = r.get(0)?;
146 let val_str: String = r.get(1)?;
147 Ok((key_str, val_str))
148 })?;
149
150 while let Some(Ok((k_str, v_str))) = rows_iter.next() {
151 // Deserialize the value from JSON
152 let value: V = serde_json::from_str(&v_str)
153 .map_err(|e| tokio_rusqlite::Error::Other(Box::new(e)))?;
154
155 // Parse the key from string
156 let key = k_str
157 .parse()
158 .map_err(|e| tokio_rusqlite::Error::Other(Box::new(e)))?;
159
160 map.insert(key, value);
161 }
162 Ok(map)
163 })
164 .await?;
165 Ok(rows)
166 }
167
168 /// Saves a key-value pair to the SQLite database.
169 ///
170 /// This method serializes the key and value to strings and inserts or
171 /// replaces them in the database.
172 async fn save(&self, key: K, value: V) -> Result<(), PersistentError> {
173 let key_str = key.to_string();
174 let val_json = serde_json::to_string(&value)?;
175
176 self.conn
177 .call(move |c| {
178 c.execute(
179 "INSERT OR REPLACE INTO kv (key, value) VALUES (?1, ?2)",
180 params![key_str, val_json],
181 )
182 .map_err(tokio_rusqlite::Error::Rusqlite)
183 })
184 .await?;
185
186 Ok(())
187 }
188
189 /// Deletes a key-value pair from the SQLite database.
190 ///
191 /// This method removes the key-value pair with the specified key from the database.
192 ///
193 /// # Errors
194 ///
195 /// Returns an error if deleting from the backend fails.
196 #[inline]
197 async fn delete(&self, key: &K) -> Result<(), PersistentError> {
198 let key_str = key.to_string();
199
200 self.conn
201 .call(move |c| {
202 c.execute("DELETE FROM kv WHERE key = ?1", params![key_str])
203 .map_err(tokio_rusqlite::Error::Rusqlite)
204 })
205 .await?;
206
207 Ok(())
208 }
209
210 /// Flushes any buffered writes to the SQLite database.
211 ///
212 /// This method ensures that all data is written to disk by executing
213 /// a PRAGMA synchronous command.
214 async fn flush(&self) -> Result<(), PersistentError> {
215 self.conn
216 .call(|c| {
217 c.execute("PRAGMA synchronous = FULL", [])
218 .map_err(tokio_rusqlite::Error::Rusqlite)
219 })
220 .await?;
221
222 Ok(())
223 }
224}