Skip to main content

sqlite_graphrag/storage/
backend.rs

1//! Storage backend abstraction layer (G14 — phase 1).
2//!
3//! Defines a trait that abstracts the database connection, enabling future
4//! migration from rusqlite to libSQL embedded replicas or other backends.
5//!
6//! Phase 1 scope: trait definition + SqliteBackend wrapper only.
7//! Phase 2 (v1.0.69+): migrate remaining 43 command handlers to use the trait.
8
9use rusqlite::Connection;
10
11/// Backend-agnostic storage abstraction.
12///
13/// Phase 1: wraps `rusqlite::Connection` without functional change.
14/// Phase 2: will be implemented for `libsql::Connection` with embedded replicas.
15pub trait StorageBackend {
16    /// Execute a SQL statement and return the number of affected rows.
17    fn execute_sql(
18        &self,
19        sql: &str,
20        params: &[&dyn rusqlite::types::ToSql],
21    ) -> Result<usize, crate::errors::AppError>;
22
23    /// Query a single row and map it with the provided closure.
24    fn query_one<T, F>(
25        &self,
26        sql: &str,
27        params: &[&dyn rusqlite::types::ToSql],
28        f: F,
29    ) -> Result<Option<T>, crate::errors::AppError>
30    where
31        F: FnOnce(&rusqlite::Row<'_>) -> Result<T, rusqlite::Error>;
32
33    /// Returns a reference to the underlying rusqlite Connection.
34    /// Phase 1 escape hatch — will be removed when full migration is complete.
35    fn as_connection(&self) -> &Connection;
36}
37
38/// Default implementation wrapping a rusqlite Connection.
39pub struct SqliteBackend {
40    conn: Connection,
41}
42
43impl SqliteBackend {
44    /// Create a new instance.
45    pub fn new(conn: Connection) -> Self {
46        Self { conn }
47    }
48
49    /// Into inner.
50    pub fn into_inner(self) -> Connection {
51        self.conn
52    }
53}
54
55impl StorageBackend for SqliteBackend {
56    fn execute_sql(
57        &self,
58        sql: &str,
59        params: &[&dyn rusqlite::types::ToSql],
60    ) -> Result<usize, crate::errors::AppError> {
61        self.conn
62            .execute(sql, params)
63            .map_err(crate::errors::AppError::Database)
64    }
65
66    fn query_one<T, F>(
67        &self,
68        sql: &str,
69        params: &[&dyn rusqlite::types::ToSql],
70        f: F,
71    ) -> Result<Option<T>, crate::errors::AppError>
72    where
73        F: FnOnce(&rusqlite::Row<'_>) -> Result<T, rusqlite::Error>,
74    {
75        match self.conn.query_row(sql, params, f) {
76            Ok(val) => Ok(Some(val)),
77            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
78            Err(e) => Err(crate::errors::AppError::Database(e)),
79        }
80    }
81
82    fn as_connection(&self) -> &Connection {
83        &self.conn
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn sqlite_backend_wraps_connection() {
93        let conn = Connection::open_in_memory().unwrap();
94        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, val TEXT)")
95            .unwrap();
96        let backend = SqliteBackend::new(conn);
97        let affected = backend
98            .execute_sql(
99                "INSERT INTO test (val) VALUES (?1)",
100                &[&"hello" as &dyn rusqlite::types::ToSql],
101            )
102            .unwrap();
103        assert_eq!(affected, 1);
104
105        let result: Option<String> = backend
106            .query_one("SELECT val FROM test WHERE id = 1", &[], |r| r.get(0))
107            .unwrap();
108        assert_eq!(result, Some("hello".to_string()));
109    }
110}