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    pub fn new(conn: Connection) -> Self {
45        Self { conn }
46    }
47
48    pub fn into_inner(self) -> Connection {
49        self.conn
50    }
51}
52
53impl StorageBackend for SqliteBackend {
54    fn execute_sql(
55        &self,
56        sql: &str,
57        params: &[&dyn rusqlite::types::ToSql],
58    ) -> Result<usize, crate::errors::AppError> {
59        self.conn
60            .execute(sql, params)
61            .map_err(crate::errors::AppError::Database)
62    }
63
64    fn query_one<T, F>(
65        &self,
66        sql: &str,
67        params: &[&dyn rusqlite::types::ToSql],
68        f: F,
69    ) -> Result<Option<T>, crate::errors::AppError>
70    where
71        F: FnOnce(&rusqlite::Row<'_>) -> Result<T, rusqlite::Error>,
72    {
73        match self.conn.query_row(sql, params, f) {
74            Ok(val) => Ok(Some(val)),
75            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
76            Err(e) => Err(crate::errors::AppError::Database(e)),
77        }
78    }
79
80    fn as_connection(&self) -> &Connection {
81        &self.conn
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn sqlite_backend_wraps_connection() {
91        let conn = Connection::open_in_memory().unwrap();
92        conn.execute_batch("CREATE TABLE test (id INTEGER PRIMARY KEY, val TEXT)")
93            .unwrap();
94        let backend = SqliteBackend::new(conn);
95        let affected = backend
96            .execute_sql(
97                "INSERT INTO test (val) VALUES (?1)",
98                &[&"hello" as &dyn rusqlite::types::ToSql],
99            )
100            .unwrap();
101        assert_eq!(affected, 1);
102
103        let result: Option<String> = backend
104            .query_one("SELECT val FROM test WHERE id = 1", &[], |r| r.get(0))
105            .unwrap();
106        assert_eq!(result, Some("hello".to_string()));
107    }
108}