Skip to main content

mirage/storage/
backend.rs

1use anyhow::Result;
2use std::path::Path;
3
4use super::{CfgBlockData, DocumentInfo, StorageTrait};
5
6#[cfg(feature = "backend-sqlite")]
7use super::sqlite_backend::SqliteStorage;
8
9/// Storage backend enum (Phase 069-01)
10///
11/// This enum wraps SqliteStorage and delegates
12/// StorageTrait methods to the appropriate implementation.
13///
14/// Follows llmgrep's Backend pattern for consistency across tools.
15#[derive(Debug)]
16#[allow(clippy::large_enum_variant)]
17pub enum Backend {
18    /// SQLite storage backend (traditional, always available)
19    #[cfg(feature = "backend-sqlite")]
20    Sqlite(SqliteStorage),
21}
22
23impl Backend {
24    /// Detect backend format from database file and open appropriate backend
25    ///
26    /// Uses file extension and magellan's detection for consistent backend detection.
27    ///
28    /// # Arguments
29    ///
30    /// * `db_path` - Path to the database file
31    ///
32    /// # Returns
33    ///
34    /// * `Ok(Backend)` - Appropriate backend variant
35    /// * `Err(...)` - Error if detection or opening fails
36    ///
37    /// # Examples
38    ///
39    /// ```ignore
40    /// # use mirage_analyzer::storage::Backend;
41    /// # fn main() -> anyhow::Result<()> {
42    /// let backend = Backend::detect_and_open("/path/to/codegraph.db")?;
43    /// # Ok(())
44    /// # }
45    /// ```
46    pub fn detect_and_open(db_path: &Path) -> Result<Self> {
47        use magellan::migrate_backend_cmd::detect_backend_format;
48
49        // For non-.geo files, use Magellan's SQLite detection.
50        let sqlite_detected = detect_backend_format(db_path).is_ok();
51
52        #[cfg(feature = "backend-sqlite")]
53        {
54            if sqlite_detected {
55                SqliteStorage::open(db_path).map(Backend::Sqlite)
56            } else {
57                Err(anyhow::anyhow!(
58                    "Unsupported database format; use a SQLite .db"
59                ))
60            }
61        }
62
63        #[cfg(not(feature = "backend-sqlite"))]
64        {
65            let _ = sqlite_detected;
66            Err(anyhow::anyhow!("No storage backend feature enabled"))
67        }
68    }
69
70    /// Check if this is a SQLite backend
71    pub fn is_sqlite(&self) -> bool {
72        match self {
73            #[cfg(feature = "backend-sqlite")]
74            Backend::Sqlite(_) => true,
75        }
76    }
77
78    /// Delegate get_cfg_blocks to inner backend
79    pub fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
80        match self {
81            #[cfg(feature = "backend-sqlite")]
82            Backend::Sqlite(s) => s.get_cfg_blocks(function_id),
83        }
84    }
85
86    /// Delegate get_entity to inner backend
87    pub fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
88        match self {
89            #[cfg(feature = "backend-sqlite")]
90            Backend::Sqlite(s) => s.get_entity(entity_id),
91        }
92    }
93
94    /// Delegate get_cached_paths to inner backend
95    pub fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
96        match self {
97            #[cfg(feature = "backend-sqlite")]
98            Backend::Sqlite(s) => s.get_cached_paths(function_id),
99        }
100    }
101
102    /// Delegate get_callees to inner backend
103    pub fn get_callees(&self, function_id: i64) -> Result<Vec<i64>> {
104        match self {
105            #[cfg(feature = "backend-sqlite")]
106            Backend::Sqlite(s) => s.get_callees(function_id),
107        }
108    }
109
110    /// Delegate list_source_documents to inner backend
111    pub fn list_source_documents(&self) -> Result<Vec<DocumentInfo>> {
112        match self {
113            #[cfg(feature = "backend-sqlite")]
114            Backend::Sqlite(s) => s.list_source_documents(),
115        }
116    }
117}
118
119// Implement StorageTrait for Backend (delegates to inner storage)
120impl StorageTrait for Backend {
121    fn get_cfg_blocks(&self, function_id: i64) -> Result<Vec<CfgBlockData>> {
122        self.get_cfg_blocks(function_id)
123    }
124
125    fn get_entity(&self, entity_id: i64) -> Option<sqlitegraph::GraphEntity> {
126        self.get_entity(entity_id)
127    }
128
129    fn get_cached_paths(&self, function_id: i64) -> Result<Option<Vec<crate::cfg::Path>>> {
130        self.get_cached_paths(function_id)
131    }
132
133    fn get_callees(&self, function_id: i64) -> Result<Vec<i64>> {
134        self.get_callees(function_id)
135    }
136}
137
138/// Database backend format detected in a graph database file.
139///
140/// This is the legacy format detection enum. For new code, use the
141/// `Backend` enum (with StorageTrait) which provides full backend abstraction.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum BackendFormat {
144    /// SQLite-based backend (default, backward compatible)
145    SQLite,
146    /// Unknown or unrecognized format
147    Unknown,
148}
149
150impl BackendFormat {
151    /// Detect which backend format a database file uses.
152    ///
153    /// Checks the file header to determine if the database is SQLite format.
154    /// Returns Unknown if the file doesn't exist or has an unrecognized header.
155    ///
156    /// **Deprecated:** Use `Backend::detect_and_open()` for new code which provides
157    /// full backend abstraction, not just format detection.
158    pub fn detect(path: &Path) -> Result<Self> {
159        if !path.exists() {
160            return Ok(BackendFormat::Unknown);
161        }
162
163        let mut file = std::fs::File::open(path)?;
164        let mut header = [0u8; 16];
165        let bytes_read = std::io::Read::read(&mut file, &mut header)?;
166
167        if bytes_read < header.len() {
168            return Ok(BackendFormat::Unknown);
169        }
170
171        // SQLite databases start with "SQLite format 3"
172        Ok(if &header[..15] == b"SQLite format 3" {
173            BackendFormat::SQLite
174        } else {
175            BackendFormat::Unknown
176        })
177    }
178}
179
180#[cfg(all(test, feature = "sqlite"))]
181mod tests {
182    use super::*;
183    use std::path::Path;
184
185    #[test]
186    fn test_backend_detect_sqlite_header() {
187        use std::io::Write;
188
189        let temp_file = tempfile::NamedTempFile::new().unwrap();
190        let mut file = std::fs::File::create(temp_file.path()).unwrap();
191        file.write_all(b"SQLite format 3\0").unwrap();
192        file.sync_all().unwrap();
193
194        let backend = BackendFormat::detect(temp_file.path()).unwrap();
195        assert_eq!(
196            backend,
197            BackendFormat::SQLite,
198            "Should detect SQLite format"
199        );
200    }
201
202    #[test]
203    fn test_backend_detect_nonexistent_file() {
204        let backend = BackendFormat::detect(Path::new("/nonexistent/path/to/file.db")).unwrap();
205        assert_eq!(
206            backend,
207            BackendFormat::Unknown,
208            "Non-existent file should be Unknown"
209        );
210    }
211
212    #[test]
213    fn test_backend_detect_empty_file() {
214        let temp_file = tempfile::NamedTempFile::new().unwrap();
215
216        let backend = BackendFormat::detect(temp_file.path()).unwrap();
217        assert_eq!(
218            backend,
219            BackendFormat::Unknown,
220            "Empty file should be Unknown"
221        );
222    }
223
224    #[test]
225    fn test_backend_detect_partial_header() {
226        use std::io::Write;
227
228        let temp_file = tempfile::NamedTempFile::new().unwrap();
229        let mut file = std::fs::File::create(temp_file.path()).unwrap();
230        file.write_all(b"SQLite").unwrap();
231        file.sync_all().unwrap();
232
233        let backend = BackendFormat::detect(temp_file.path()).unwrap();
234        assert_eq!(
235            backend,
236            BackendFormat::Unknown,
237            "Partial header should be Unknown"
238        );
239    }
240
241    #[test]
242    fn test_backend_equality() {
243        assert_eq!(BackendFormat::SQLite, BackendFormat::SQLite);
244        assert_eq!(BackendFormat::Unknown, BackendFormat::Unknown);
245
246        assert_ne!(BackendFormat::SQLite, BackendFormat::Unknown);
247    }
248}