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}