use anyhow::Result;
use rusqlite::Connection;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::path::PathBuf;
pub struct ModulePathIndex {
module_to_file: HashMap<String, PathBuf>,
}
impl ModulePathIndex {
pub fn build(db_path: &PathBuf) -> Result<Self> {
let conn = Connection::open(db_path)?;
let query = r#"
SELECT file_path, data
FROM graph_entities
WHERE kind = 'Symbol'
AND data LIKE '%display_fqn%'
LIMIT 10000
"#;
let mut stmt = conn.prepare(query)?;
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
let mut module_to_file = HashMap::new();
for row_result in rows {
let (file_path, data_str) = row_result?;
if let Ok(data) = serde_json::from_str::<JsonValue>(&data_str) {
if let Some(display_fqn) = data.get("display_fqn").and_then(|v| v.as_str()) {
let parts: Vec<&str> = display_fqn.split("::").collect();
if parts.len() > 1 {
let module_path = parts[..parts.len() - 1].join("::");
module_to_file.insert(module_path, PathBuf::from(&file_path));
}
}
}
}
Ok(Self { module_to_file })
}
pub fn resolve(&self, module_path: &str) -> Option<&PathBuf> {
self.module_to_file.get(module_path)
}
pub fn len(&self) -> usize {
self.module_to_file.len()
}
pub fn is_empty(&self) -> bool {
self.module_to_file.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::params;
use tempfile::TempDir;
#[test]
fn test_build_index() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("splice.db");
let module_file = temp_dir.path().join("src/completion/types.rs");
std::fs::create_dir_all(module_file.parent().unwrap()).unwrap();
std::fs::write(&module_file, "pub struct CompletionRequest;\n").unwrap();
let conn = Connection::open(&db_path).unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
file_path TEXT NOT NULL,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO graph_entities (id, name, kind, file_path, data)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
1_i64,
"CompletionRequest",
"Symbol",
module_file.to_string_lossy().as_ref(),
r#"{"display_fqn":"splice::completion::types::CompletionRequest"}"#
],
)
.unwrap();
drop(conn);
let result = ModulePathIndex::build(&db_path);
assert!(result.is_ok(), "Failed to build index: {:?}", result.err());
let index = result.unwrap();
assert_eq!(index.len(), 1);
let resolved = index
.resolve("splice::completion::types")
.expect("module path should resolve");
assert_eq!(resolved, &module_file);
assert!(resolved.exists(), "File path should exist");
}
#[test]
fn test_resolve_unknown_module() {
let index = ModulePathIndex {
module_to_file: HashMap::new(),
};
assert!(index.resolve("unknown::module").is_none());
}
#[test]
fn test_len_and_is_empty() {
let empty_index = ModulePathIndex {
module_to_file: HashMap::new(),
};
assert_eq!(empty_index.len(), 0);
assert!(empty_index.is_empty());
let mut populated = HashMap::new();
populated.insert("test::module".to_string(), PathBuf::from("/test/path.rs"));
let populated_index = ModulePathIndex {
module_to_file: populated,
};
assert_eq!(populated_index.len(), 1);
assert!(!populated_index.is_empty());
}
}