use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use anyhow::Result;
use memmap2::Mmap;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use crate::db;
use crate::types::SymbolRecord;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphSnapshot {
pub version: u32,
pub symbols: Vec<SymbolEntry>,
pub edges: Vec<(u32, u32)>,
pub name_index: HashMap<String, Vec<u32>>,
pub qualified_index: HashMap<String, Vec<u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolEntry {
pub id: i64,
pub name: String,
pub qualified_name: String,
pub kind: String,
pub file_id: i64,
pub path: String,
pub language: String,
pub start_line: u32,
pub end_line: u32,
pub signature: String,
pub exported: bool,
}
impl SymbolEntry {
pub fn to_record(&self) -> SymbolRecord {
SymbolRecord {
id: self.id,
name: self.name.clone(),
qualified_name: self.qualified_name.clone(),
kind: self.kind.clone(),
file_id: self.file_id,
path: self.path.clone(),
language: self.language.clone(),
start_line: self.start_line as usize,
end_line: self.end_line as usize,
signature: self.signature.clone(),
exported: self.exported,
}
}
}
pub const SNAPSHOT_VERSION: u32 = 1;
pub fn build(conn: &Connection, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
let mut id_to_index: HashMap<i64, u32> = HashMap::new();
let mut symbols: Vec<SymbolEntry> = Vec::new();
let mut name_index: HashMap<String, Vec<u32>> = HashMap::new();
let mut qualified_index: HashMap<String, Vec<u32>> = HashMap::new();
{
let mut stmt = conn.prepare(
"
SELECT s.id, s.name, s.qualified_name, s.kind, s.file_id, f.path, f.language,
s.start_line, s.end_line, s.signature, s.exported
FROM symbols s
JOIN files f ON f.id = s.file_id
ORDER BY s.id
",
)?;
let rows = stmt.query_map([], db::map_symbol)?;
for row in rows {
let record = row?;
let idx = symbols.len() as u32;
id_to_index.insert(record.id, idx);
name_index.entry(record.name.clone()).or_default().push(idx);
qualified_index
.entry(record.qualified_name.clone())
.or_default()
.push(idx);
symbols.push(SymbolEntry {
id: record.id,
name: record.name,
qualified_name: record.qualified_name,
kind: record.kind,
file_id: record.file_id,
path: record.path,
language: record.language,
start_line: record.start_line as u32,
end_line: record.end_line as u32,
signature: record.signature,
exported: record.exported,
});
}
}
let mut edges: Vec<(u32, u32)> = Vec::new();
{
let mut stmt = conn.prepare(
"
SELECT e.from_symbol_id, e.to_symbol_name
FROM edges e
",
)?;
let rows = stmt.query_map([], |row| {
let from: i64 = row.get(0)?;
let to_name: String = row.get(1)?;
Ok((from, to_name))
})?;
for row in rows {
let (from, to_name) = row?;
let Some(&from_idx) = id_to_index.get(&from) else {
continue;
};
let candidates = qualified_index
.get(&to_name)
.or_else(|| name_index.get(&to_name));
if let Some(targets) = candidates {
for &to_idx in targets {
edges.push((from_idx, to_idx));
}
}
}
}
let snapshot = GraphSnapshot {
version: SNAPSHOT_VERSION,
symbols,
edges,
name_index,
qualified_index,
};
let encoded = bincode::serialize(&snapshot)?;
let tmp_path = path.with_extension("bin.tmp");
{
let mut file = File::create(&tmp_path)?;
file.write_all(&encoded)?;
file.sync_all()?;
}
fs::rename(&tmp_path, path)?;
Ok(())
}
pub struct MappedSnapshot {
_mmap: Mmap,
snapshot: GraphSnapshot,
}
impl MappedSnapshot {
pub fn open(path: &Path) -> Result<Self> {
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
let snapshot: GraphSnapshot = bincode::deserialize(&mmap[..])?;
Ok(Self {
_mmap: mmap,
snapshot,
})
}
pub fn graph(&self) -> &GraphSnapshot {
&self.snapshot
}
}
pub fn try_open(path: &Path) -> Option<MappedSnapshot> {
MappedSnapshot::open(path).ok()
}