use crate::error::LlmError;
use crate::output::{
CallSearchResponse, DocsSearchResponse, FactsSearchResponse, ImplementsSearchResponse,
ReferenceSearchResponse, SearchResponse,
};
use crate::query::{DocsSearchOptions, FactsSearchOptions, SearchOptions};
use std::path::Path;
#[cfg(feature = "geometric-backend")]
pub mod geometric;
#[cfg(feature = "geometric-backend")]
pub mod magellan_adapter; pub mod schema_check;
pub mod sqlite;
#[cfg(feature = "geometric-backend")]
pub use geometric::GeometricBackend;
#[cfg(feature = "geometric-backend")]
pub use magellan_adapter::{
apply_path_filter, lookup_symbol_by_path_and_name, normalize_path_for_query, paths_equivalent,
SymbolLookupResult,
};
pub use sqlite::SqliteBackend;
pub trait BackendTrait {
fn search_symbols(
&self,
options: SearchOptions,
) -> Result<(SearchResponse, bool, bool), LlmError>;
fn search_references(
&self,
options: SearchOptions,
) -> Result<(ReferenceSearchResponse, bool), LlmError>;
fn search_calls(&self, options: SearchOptions) -> Result<(CallSearchResponse, bool), LlmError>;
fn search_implements(
&self,
options: SearchOptions,
) -> Result<(ImplementsSearchResponse, bool), LlmError>;
fn search_docs(&self, options: DocsSearchOptions) -> Result<DocsSearchResponse, LlmError>;
fn search_facts(&self, options: FactsSearchOptions) -> Result<FactsSearchResponse, LlmError>;
fn ast(
&self,
file: &Path,
position: Option<usize>,
limit: usize,
) -> Result<serde_json::Value, LlmError>;
fn find_ast(&self, kind: &str) -> Result<serde_json::Value, LlmError>;
fn complete(&self, prefix: &str, limit: usize) -> Result<Vec<String>, LlmError>;
fn lookup(&self, fqn: &str, db_path: &str) -> Result<crate::output::SymbolMatch, LlmError>;
fn search_by_label(
&self,
label: &str,
limit: usize,
db_path: &str,
) -> Result<(SearchResponse, bool, bool), LlmError>;
#[cfg(feature = "geometric-backend")]
fn get_chunks_for_symbol(
&self,
file_path: &str,
symbol_name: &str,
) -> Result<Vec<crate::backend::magellan_adapter::CodeChunk>, LlmError>;
}
#[derive(Debug)]
pub enum Backend {
Sqlite(SqliteBackend),
#[cfg(feature = "geometric-backend")]
Geometric(GeometricBackend),
}
fn is_geometric_extension(path: &std::path::Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.map(|e| e == "geo")
.unwrap_or(false)
}
impl Backend {
pub fn detect_and_open(db_path: &Path) -> Result<Self, LlmError> {
use std::fs::File;
use std::io::Read;
if !db_path.exists() {
return Err(LlmError::DatabaseNotFound {
path: db_path.display().to_string(),
});
}
#[cfg(feature = "geometric-backend")]
{
if is_geometric_extension(db_path) {
return GeometricBackend::open(db_path).map(Backend::Geometric);
}
}
#[cfg(not(feature = "geometric-backend"))]
{
if is_geometric_extension(db_path) {
return Err(LlmError::BackendDetectionFailed {
path: db_path.display().to_string(),
reason: "Geometric backend (.geo files) requires 'geometric-backend' feature"
.to_string(),
});
}
}
let mut file = File::open(db_path).map_err(|e| LlmError::BackendDetectionFailed {
path: db_path.display().to_string(),
reason: format!("Cannot open file: {}", e),
})?;
let mut header = [0u8; 16];
file.read_exact(&mut header)
.map_err(|e| LlmError::BackendDetectionFailed {
path: db_path.display().to_string(),
reason: format!("Cannot read file header: {}", e),
})?;
let is_sqlite = &header[0..16] == b"SQLite format 3\0";
if is_sqlite {
SqliteBackend::open(db_path).map(Backend::Sqlite)
} else {
SqliteBackend::open(db_path).map(Backend::Sqlite)
}
}
pub fn search_symbols(
&self,
options: SearchOptions,
) -> Result<(SearchResponse, bool, bool), LlmError> {
match self {
Backend::Sqlite(b) => b.search_symbols(options),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_symbols(options),
}
}
pub fn search_references(
&self,
options: SearchOptions,
) -> Result<(ReferenceSearchResponse, bool), LlmError> {
match self {
Backend::Sqlite(b) => b.search_references(options),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_references(options),
}
}
pub fn search_calls(
&self,
options: SearchOptions,
) -> Result<(CallSearchResponse, bool), LlmError> {
match self {
Backend::Sqlite(b) => b.search_calls(options),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_calls(options),
}
}
pub fn search_implements(
&self,
options: SearchOptions,
) -> Result<(ImplementsSearchResponse, bool), LlmError> {
match self {
Backend::Sqlite(b) => b.search_implements(options),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_implements(options),
}
}
pub fn search_docs(
&self,
options: DocsSearchOptions,
) -> Result<DocsSearchResponse, LlmError> {
match self {
Backend::Sqlite(b) => b.search_docs(options),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_docs(options),
}
}
pub fn search_facts(
&self,
options: FactsSearchOptions,
) -> Result<FactsSearchResponse, LlmError> {
match self {
Backend::Sqlite(b) => b.search_facts(options),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_facts(options),
}
}
pub fn ast(
&self,
file: &Path,
position: Option<usize>,
limit: usize,
) -> Result<serde_json::Value, LlmError> {
match self {
Backend::Sqlite(b) => b.ast(file, position, limit),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.ast(file, position, limit),
}
}
pub fn find_ast(&self, kind: &str) -> Result<serde_json::Value, LlmError> {
match self {
Backend::Sqlite(b) => b.find_ast(kind),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.find_ast(kind),
}
}
pub fn complete(&self, prefix: &str, limit: usize) -> Result<Vec<String>, LlmError> {
match self {
Backend::Sqlite(b) => b.complete(prefix, limit),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.complete(prefix, limit),
}
}
pub fn lookup(&self, fqn: &str, db_path: &str) -> Result<crate::output::SymbolMatch, LlmError> {
match self {
Backend::Sqlite(b) => b.lookup(fqn, db_path),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.lookup(fqn, db_path),
}
}
pub fn search_by_label(
&self,
label: &str,
limit: usize,
db_path: &str,
) -> Result<(SearchResponse, bool, bool), LlmError> {
match self {
Backend::Sqlite(b) => b.search_by_label(label, limit, db_path),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.search_by_label(label, limit, db_path),
}
}
#[cfg(feature = "geometric-backend")]
pub fn get_chunks_for_symbol(
&self,
file_path: &str,
symbol_name: &str,
) -> Result<Vec<crate::backend::magellan_adapter::CodeChunk>, LlmError> {
match self {
Backend::Sqlite(_) => Err(LlmError::ChunksNotAvailable {
backend: "SQLite".to_string(),
message:
"SQLite backend does not support chunk retrieval. Use Geometric (.geo) backend."
.to_string(),
}),
#[cfg(feature = "geometric-backend")]
Backend::Geometric(b) => b.get_chunks_for_symbol(file_path, symbol_name),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[cfg(feature = "geometric-backend")]
fn create_test_geo_db() -> (tempfile::TempDir, std::path::PathBuf) {
let temp_dir = tempfile::TempDir::new().unwrap();
let geo_path = temp_dir.path().join("test.geo");
let _backend = magellan::graph::geometric_backend::GeometricBackend::create(&geo_path)
.expect("Failed to create test geo database");
(temp_dir, geo_path)
}
#[test]
#[cfg(feature = "geometric-backend")]
fn test_detect_and_open_geometric_backend() {
let (_temp_dir, geo_path) = create_test_geo_db();
let result = Backend::detect_and_open(&geo_path);
assert!(
result.is_ok(),
"Layer 1: Should detect and open .geo file: {:?}",
result.err()
);
match result.unwrap() {
Backend::Geometric(_) => {
}
_ => panic!("Layer 2: Expected Geometric backend variant"),
}
}
#[test]
fn test_detect_and_open_sqlite_backend() {
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"SQLite format 3\0").unwrap();
let result = Backend::detect_and_open(temp_file.path());
if result.is_ok() {
let backend = result.unwrap();
assert!(
matches!(backend, Backend::Sqlite(_)),
"Layer 2: Expected Sqlite backend variant for SQLite header"
);
}
}
#[test]
fn test_detect_and_open_nonexistent_file() {
let fake_path = Path::new("/nonexistent/path/code.geo");
let result = Backend::detect_and_open(fake_path);
assert!(
result.is_err(),
"Layer 1: Should fail for non-existent file"
);
match result {
Err(LlmError::DatabaseNotFound { .. }) => {
}
_ => panic!("Layer 2: Expected DatabaseNotFound error"),
}
}
}