use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use rusqlite::Connection;
use crate::config;
fn default_meta_db_path() -> PathBuf {
std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|_| std::env::var("HOME").map(|h| PathBuf::from(h).join(".local/share")))
.unwrap_or_else(|_| PathBuf::from(".atheneum"))
.join("atheneum")
.join("meta.db")
}
#[derive(Debug, Clone)]
pub struct ProjectInfo {
pub name: String,
pub root_path: String,
pub magellan_db: String,
pub atheneum_db: Option<String>,
pub language: Option<String>,
pub enabled: bool,
pub last_indexed: Option<String>,
pub file_count: i64,
pub symbol_count: i64,
pub created_at: String,
}
pub struct MetaRouter {
conn: Connection,
path: PathBuf,
}
impl MetaRouter {
pub fn open() -> Result<Self> {
let path = config::load()
.map(|cfg| cfg.meta_db_path())
.unwrap_or_else(|_| default_meta_db_path());
Self::open_at(path)
}
pub fn open_at<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent dir for {}", path.display()))?;
}
let conn = Connection::open(&path)
.with_context(|| format!("Failed to open meta.db at {}", path.display()))?;
let mut router = Self { conn, path };
router.ensure_schema()?;
Ok(router)
}
fn ensure_schema(&mut self) -> Result<()> {
self.conn.execute(
"CREATE TABLE IF NOT EXISTS project_registry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
root_path TEXT NOT NULL,
magellan_db TEXT NOT NULL,
atheneum_db TEXT,
language TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
last_indexed TIMESTAMP,
file_count INTEGER DEFAULT 0,
symbol_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_registry_enabled
ON project_registry (enabled)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_registry_lang
ON project_registry (language) WHERE enabled = 1",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS symbol_index (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT NOT NULL REFERENCES project_registry(name),
symbol_name TEXT NOT NULL,
kind TEXT,
file_path TEXT,
line INTEGER,
UNIQUE(project, symbol_name, file_path)
)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_symbol_name ON symbol_index(symbol_name)",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS symbol_analogies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_project TEXT NOT NULL REFERENCES project_registry(name),
from_symbol TEXT NOT NULL,
to_project TEXT NOT NULL REFERENCES project_registry(name),
to_symbol TEXT NOT NULL,
similarity_score REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_analogies_from
ON symbol_analogies (from_project, from_symbol)",
[],
)?;
Ok(())
}
pub fn register_project(
&mut self,
name: &str,
root_path: &str,
magellan_db: &str,
atheneum_db: Option<&str>,
language: Option<&str>,
) -> Result<()> {
self.conn.execute(
"INSERT INTO project_registry
(name, root_path, magellan_db, atheneum_db, language, enabled, last_indexed)
VALUES (?1, ?2, ?3, ?4, ?5, 1, CURRENT_TIMESTAMP)
ON CONFLICT(name) DO UPDATE SET
root_path = excluded.root_path,
magellan_db = excluded.magellan_db,
atheneum_db = excluded.atheneum_db,
language = excluded.language,
enabled = 1,
last_indexed = CURRENT_TIMESTAMP",
rusqlite::params![name, root_path, magellan_db, atheneum_db, language],
)?;
Ok(())
}
pub fn list_projects(&self) -> Result<Vec<ProjectInfo>> {
let mut stmt = self.conn.prepare(
"SELECT name, root_path, magellan_db, atheneum_db, language,
enabled, last_indexed, file_count, symbol_count, created_at
FROM project_registry
WHERE enabled = 1
ORDER BY name",
)?;
let rows = stmt.query_map([], |row| {
Ok(ProjectInfo {
name: row.get(0)?,
root_path: row.get(1)?,
magellan_db: row.get(2)?,
atheneum_db: row.get(3)?,
language: row.get(4)?,
enabled: row.get::<_, i64>(5)? != 0,
last_indexed: row.get(6)?,
file_count: row.get(7)?,
symbol_count: row.get(8)?,
created_at: row.get(9)?,
})
})?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
Ok(out)
}
pub fn list_projects_by_language(&self, language: &str) -> Result<Vec<ProjectInfo>> {
let mut stmt = self.conn.prepare(
"SELECT name, root_path, magellan_db, atheneum_db, language,
enabled, last_indexed, file_count, symbol_count, created_at
FROM project_registry
WHERE enabled = 1 AND language = ?1
ORDER BY name",
)?;
let rows = stmt.query_map([language], |row| {
Ok(ProjectInfo {
name: row.get(0)?,
root_path: row.get(1)?,
magellan_db: row.get(2)?,
atheneum_db: row.get(3)?,
language: row.get(4)?,
enabled: row.get::<_, i64>(5)? != 0,
last_indexed: row.get(6)?,
file_count: row.get(7)?,
symbol_count: row.get(8)?,
created_at: row.get(9)?,
})
})?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
Ok(out)
}
pub fn get_project(&self, name: &str) -> Result<Option<ProjectInfo>> {
let mut stmt = self.conn.prepare(
"SELECT name, root_path, magellan_db, atheneum_db, language,
enabled, last_indexed, file_count, symbol_count, created_at
FROM project_registry
WHERE name = ?1",
)?;
let row = stmt.query_row([name], |row| {
Ok(ProjectInfo {
name: row.get(0)?,
root_path: row.get(1)?,
magellan_db: row.get(2)?,
atheneum_db: row.get(3)?,
language: row.get(4)?,
enabled: row.get::<_, i64>(5)? != 0,
last_indexed: row.get(6)?,
file_count: row.get(7)?,
symbol_count: row.get(8)?,
created_at: row.get(9)?,
})
});
match row {
Ok(p) => Ok(Some(p)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn disable_project(&mut self, name: &str) -> Result<()> {
self.conn.execute(
"UPDATE project_registry SET enabled = 0 WHERE name = ?1",
[name],
)?;
Ok(())
}
pub fn path(&self) -> &Path {
&self.path
}
pub(crate) fn conn(&self) -> &Connection {
&self.conn
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_meta_router_schema_and_roundtrip() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let mut router = MetaRouter::open_at(tmp.path()).unwrap();
router
.register_project(
"envoy",
"/home/feanor/Projects/envoy",
"/home/feanor/Projects/envoy/.magellan/magellan.db",
Some("/home/feanor/Projects/envoy/atheneum.db"),
Some("rust"),
)
.unwrap();
router
.register_project(
"magellan",
"/home/feanor/Projects/magellan",
"/home/feanor/Projects/magellan/.magellan/magellan.db",
None,
Some("rust"),
)
.unwrap();
let projects = router.list_projects().unwrap();
assert_eq!(projects.len(), 2);
assert_eq!(projects[0].name, "envoy");
assert_eq!(projects[0].language.as_deref(), Some("rust"));
assert!(projects[0].enabled);
let rust_projects = router.list_projects_by_language("rust").unwrap();
assert_eq!(rust_projects.len(), 2);
let envoy = router.get_project("envoy").unwrap();
assert!(envoy.is_some());
assert_eq!(envoy.unwrap().root_path, "/home/feanor/Projects/envoy");
router.disable_project("envoy").unwrap();
let after = router.list_projects().unwrap();
assert_eq!(after.len(), 1);
assert_eq!(after[0].name, "magellan");
}
#[test]
fn test_meta_router_upsert() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let mut router = MetaRouter::open_at(tmp.path()).unwrap();
router
.register_project("test", "/old", "/old/magellan.db", None, Some("python"))
.unwrap();
router
.register_project(
"test",
"/new",
"/new/magellan.db",
Some("/new/atheneum.db"),
Some("rust"),
)
.unwrap();
let p = router.get_project("test").unwrap().unwrap();
assert_eq!(p.root_path, "/new");
assert_eq!(p.language.as_deref(), Some("rust"));
assert_eq!(p.atheneum_db.as_deref(), Some("/new/atheneum.db"));
}
}