use crate::error::LlmError;
use crate::output::{
CallSearchResponse, DocsSearchResponse, FactsSearchResponse, ImplementsSearchResponse,
ReferenceSearchResponse, SearchResponse,
};
use crate::query::{DocsSearchOptions, FactsSearchOptions, SearchOptions};
use std::path::Path;
pub mod schema_check;
pub mod sqlite;
pub mod vector;
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>;
}
#[derive(Debug)]
pub enum Backend {
Sqlite(SqliteBackend),
}
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(),
});
}
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),
}
}
pub fn search_references(
&self,
options: SearchOptions,
) -> Result<(ReferenceSearchResponse, bool), LlmError> {
match self {
Backend::Sqlite(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),
}
}
pub fn search_implements(
&self,
options: SearchOptions,
) -> Result<(ImplementsSearchResponse, bool), LlmError> {
match self {
Backend::Sqlite(b) => b.search_implements(options),
}
}
pub fn search_docs(&self, options: DocsSearchOptions) -> Result<DocsSearchResponse, LlmError> {
match self {
Backend::Sqlite(b) => b.search_docs(options),
}
}
pub fn search_facts(
&self,
options: FactsSearchOptions,
) -> Result<FactsSearchResponse, LlmError> {
match self {
Backend::Sqlite(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),
}
}
pub fn find_ast(&self, kind: &str) -> Result<serde_json::Value, LlmError> {
match self {
Backend::Sqlite(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),
}
}
pub fn lookup(&self, fqn: &str, db_path: &str) -> Result<crate::output::SymbolMatch, LlmError> {
match self {
Backend::Sqlite(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(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[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 let Ok(backend) = result {
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"),
}
}
}