use anyhow::Result;
use std::path::{Path, PathBuf};
use sqlitegraph::{GraphBackend, NodeId, SnapshotId};
use super::query;
use super::CodeGraph;
use crate::common::extract_symbol_content_safe;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReconcileOutcome {
Deleted,
Unchanged,
Reindexed {
symbols: usize,
references: usize,
calls: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteResult {
pub symbols_deleted: usize,
pub references_deleted: usize,
pub calls_deleted: usize,
pub chunks_deleted: usize,
pub ast_nodes_deleted: usize,
pub cfg_blocks_deleted: usize,
pub edges_deleted: usize,
}
impl DeleteResult {
pub fn total_deleted(&self) -> usize {
self.symbols_deleted
+ self.references_deleted
+ self.calls_deleted
+ self.chunks_deleted
+ self.ast_nodes_deleted
+ self.cfg_blocks_deleted
+ self.edges_deleted
}
pub fn is_empty(&self) -> bool {
self.total_deleted() == 0
}
}
pub fn index_file(graph: &mut CodeGraph, path: &str, source: &[u8]) -> Result<usize> {
use crate::generation::CodeChunk;
use crate::ingest::c::CParser;
use crate::ingest::cpp::CppParser;
use crate::ingest::java::JavaParser;
use crate::ingest::javascript::JavaScriptParser;
use crate::ingest::pool;
use crate::ingest::python::PythonParser;
use crate::ingest::typescript::TypeScriptParser;
use crate::ingest::{detect::Language, detect_language, Parser};
let hash = graph.files.compute_hash(source);
let file_id = graph.files.find_or_create_file_node(path, &hash)?;
graph.symbols.delete_file_symbols(file_id)?;
let path_buf = PathBuf::from(path);
let language = detect_language(&path_buf);
let parsed_tree = match language {
Some(lang) => match pool::with_parser(lang, |parser| parser.parse(source, None)) {
Ok(tree) => tree,
Err(e) => {
eprintln!("Warning: Failed to parse {} for indexing: {}", path, e);
None
}
},
None => None,
};
let symbol_facts = match (language, &parsed_tree) {
(Some(Language::Rust), Some(tree)) => {
Parser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
(Some(Language::C), Some(tree)) => {
CParser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
(Some(Language::Cpp), Some(tree)) => {
CppParser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
(Some(Language::Java), Some(tree)) => {
JavaParser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
(Some(Language::Python), Some(tree)) => {
PythonParser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
(Some(Language::JavaScript), Some(tree)) => {
JavaScriptParser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
(Some(Language::TypeScript), Some(tree)) => {
TypeScriptParser::extract_symbols_from_tree(tree, path_buf.clone(), source)
}
_ => Vec::new(),
};
let mut function_symbol_ids: Vec<(String, i64, i64, i64)> = Vec::new();
let mut indexed_symbols: Vec<(crate::ingest::SymbolFact, i64)> = Vec::new();
let language_label = language.map(|l| l.as_str().to_string());
let symbol_ids = graph.symbols.insert_symbol_nodes_batch(&symbol_facts)?;
graph
.symbols
.insert_defines_edges_batch(file_id, &symbol_ids)?;
for (i, fact) in symbol_facts.iter().enumerate() {
let symbol_id = symbol_ids[i];
if let Some(ref lang) = language_label {
let _ = graph.add_label(symbol_id.as_i64(), lang);
}
let _ = graph.add_label(symbol_id.as_i64(), &fact.kind_normalized);
if fact.kind_normalized == "fn" || fact.kind_normalized == "method" {
if let Some(ref name) = fact.name {
function_symbol_ids.push((
name.clone(),
symbol_id.as_i64(),
fact.byte_start as i64,
fact.byte_end as i64,
));
}
}
indexed_symbols.push((fact.clone(), symbol_id.as_i64()));
}
let mut code_chunks = Vec::new();
for fact in &symbol_facts {
if let Some(content) = extract_symbol_content_safe(source, fact.byte_start, fact.byte_end) {
let chunk = CodeChunk::new(
path.to_string(),
fact.byte_start,
fact.byte_end,
content,
fact.name.clone(),
Some(fact.kind_normalized.clone()),
);
code_chunks.push(chunk);
}
}
if !code_chunks.is_empty() {
graph.store_code_chunks(&code_chunks)?;
}
if let Some(ref tree) = parsed_tree {
let ast_nodes = crate::graph::extract_ast_nodes(tree, source);
if !ast_nodes.is_empty() {
insert_ast_nodes(graph, file_id.as_i64(), ast_nodes)?;
}
}
if let (Some(Language::Rust), Some(ref tree)) = (language, &parsed_tree) {
let impl_relations =
crate::ingest::Parser::extract_impl_relations_static(tree, source, &path_buf);
let fqn_map = graph.symbols.lookup.fqn_to_id_with_current_file(path);
let mut simple_name_map: std::collections::HashMap<String, i64> =
std::collections::HashMap::new();
for (fqn, (id, _is_current)) in &fqn_map {
if let Some(simple) = fqn.rsplit("::").next() {
simple_name_map.entry(simple.to_string()).or_insert(*id);
}
}
for rel in &impl_relations {
let Some(ref trait_name) = rel.trait_name else {
continue;
};
let type_id = simple_name_map.get(&rel.type_name).copied();
let trait_id = simple_name_map.get(trait_name).copied();
if let (Some(type_id), Some(trait_id)) = (type_id, trait_id) {
let type_node_id = NodeId::from(type_id);
let trait_node_id = NodeId::from(trait_id);
if let Err(e) = graph
.symbols
.insert_implements_edge(type_node_id, trait_node_id)
{
eprintln!(
"Warning: Failed to insert IMPLEMENTS edge {} -> {}: {}",
rel.type_name, trait_name, e
);
}
}
}
}
if let (Some(Language::Rust), Some(ref tree)) = (language, &parsed_tree) {
let import_extractor = crate::ingest::imports::ImportExtractor::default();
let extracted_imports =
import_extractor.extract_imports_from_tree(&tree.root_node(), source, &path_buf);
if !extracted_imports.is_empty() {
let _ = graph.imports.delete_imports_in_file(path);
let _ = graph.imports.index_imports(
path,
file_id.as_i64(),
extracted_imports,
Some(&graph.module_resolver),
);
}
}
let is_rust = path.ends_with(".rs");
let is_cpp = path.ends_with(".cpp")
|| path.ends_with(".hpp")
|| path.ends_with(".cc")
|| path.ends_with(".cxx");
let is_c = path.ends_with(".c") || path.ends_with(".h");
if (is_rust || is_c || is_cpp) && !function_symbol_ids.is_empty() {
if let Some(ref tree) = parsed_tree {
let root = tree.root_node();
let function_kind = if is_rust {
"function_item"
} else {
"function_definition"
};
let mut function_nodes = Vec::new();
fn find_function_nodes_recursive<'a>(
node: tree_sitter::Node<'a>,
function_kind: &str,
result: &mut Vec<tree_sitter::Node<'a>>,
) {
if node.kind() == function_kind {
result.push(node);
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
find_function_nodes_recursive(cursor.node(), function_kind, result);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
find_function_nodes_recursive(root, function_kind, &mut function_nodes);
for func_node in function_nodes {
let func_start = func_node.byte_range().start as i64;
let func_end = func_node.byte_range().end as i64;
if let Some((_, entity_id, _, _)) = function_symbol_ids
.iter()
.find(|(_, _, start, end)| func_start >= *start && func_end <= *end)
{
let _ = graph
.cfg_ops
.index_cfg_with_4d_coordinates_from_node(&func_node, source, *entity_id);
}
}
}
}
if let (Some(ref tree), Some(lang)) = (parsed_tree, language) {
let _ = super::calls::index_calls_with_tree(graph, path, source, tree, lang);
}
use crate::graph::schema::SymbolNode;
let symbol_nodes: Vec<SymbolNode> = symbol_facts
.iter()
.filter_map(|fact| {
if fact.start_line == 0 && fact.byte_start == 0 {
return None;
}
Some(SymbolNode {
symbol_id: None, name: fact.name.clone(),
kind: fact.kind_normalized.clone(),
kind_normalized: Some(fact.kind_normalized.clone()),
fqn: fact.fqn.clone(),
display_fqn: fact.display_fqn.clone(),
canonical_fqn: fact.canonical_fqn.clone(),
byte_start: fact.byte_start,
byte_end: fact.byte_end,
start_line: fact.start_line,
start_col: fact.start_col,
end_line: fact.end_line,
end_col: fact.end_col,
})
})
.collect();
if let Err(e) = graph.metrics.compute_for_file(path, source, &symbol_nodes) {
eprintln!("Warning: Failed to compute metrics for '{}': {}", path, e);
}
graph.invalidate_cache(path);
Ok(symbol_facts.len())
}
pub fn delete_file(graph: &mut CodeGraph, path: &str) -> Result<DeleteResult> {
delete_file_facts(graph, path)
}
pub fn delete_file_facts(graph: &mut CodeGraph, path: &str) -> Result<DeleteResult> {
let all_file_nodes = graph.files.find_all_file_nodes(path)?;
if all_file_nodes.len() > 1 {
for (dup_id, _) in all_file_nodes.iter().skip(1) {
let _ = graph.files.backend.delete_entity(dup_id.as_i64());
}
let normalized_path = crate::graph::files::normalize_path_for_index(path);
graph.files.file_index.remove(&normalized_path);
if let Some((remaining_id, _)) = all_file_nodes.first() {
graph
.files
.file_index
.insert(normalized_path, *remaining_id);
}
}
let snapshot = SnapshotId::current();
let expected_symbols: usize = if let Some(file_id) = graph.files.find_file_node(path)? {
graph
.files
.backend
.neighbors(
snapshot,
file_id.as_i64(),
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Outgoing,
edge_type: Some("DEFINES".to_string()),
},
)
.map(|ids| ids.len())
.unwrap_or(0)
} else {
0
};
let expected_references = count_references_in_file(graph, path);
let expected_calls = count_calls_in_file(graph, path);
let expected_chunks = count_chunks_for_file(graph, path);
let expected_ast_nodes = count_ast_nodes_for_file(graph, path);
let _expected_cfg_blocks = count_cfg_blocks_for_file(graph, path);
let mut deleted_entity_ids: Vec<i64> = Vec::new();
let symbols_deleted: usize;
let chunks_deleted: usize;
let references_deleted: usize;
let calls_deleted: usize;
let ast_nodes_deleted: usize;
let cfg_blocks_deleted: usize;
let edges_deleted: usize;
if let Some(file_id) = graph.files.find_file_node(path)? {
let snapshot = SnapshotId::current();
let symbol_ids = match graph.files.backend.neighbors(
snapshot,
file_id.as_i64(),
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Outgoing,
edge_type: Some("DEFINES".to_string()),
},
) {
Ok(ids) => ids,
Err(sqlitegraph::SqliteGraphError::NotFound(_)) => {
let normalized_path = crate::graph::files::normalize_path_for_index(path);
graph.files.file_index.remove(&normalized_path);
return Ok(DeleteResult {
symbols_deleted: 0,
references_deleted: 0,
calls_deleted: 0,
chunks_deleted: 0,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted: 0,
});
}
Err(e) => return Err(e.into()),
};
let mut symbol_ids_sorted = symbol_ids;
symbol_ids_sorted.sort_unstable();
let blocks = graph.cfg_ops.delete_cfg_for_functions(&symbol_ids_sorted)?;
cfg_blocks_deleted = blocks;
edges_deleted = blocks;
for symbol_id in &symbol_ids_sorted {
graph.files.backend.delete_entity(*symbol_id)?;
}
symbols_deleted = symbol_ids_sorted.len();
deleted_entity_ids.extend(symbol_ids_sorted.iter().copied());
assert_eq!(
symbols_deleted, expected_symbols,
"Symbol deletion count mismatch for '{}': expected {}, got {}",
path, expected_symbols, symbols_deleted
);
graph.files.backend.delete_entity(file_id.as_i64())?;
deleted_entity_ids.push(file_id.as_i64());
references_deleted = graph.references.delete_references_in_file(path)?;
assert_eq!(
references_deleted, expected_references,
"Reference deletion count mismatch for '{}': expected {}, got {}",
path, expected_references, references_deleted
);
calls_deleted = graph.calls.delete_calls_in_file(path)?;
assert_eq!(
calls_deleted, expected_calls,
"Call deletion count mismatch for '{}': expected {}, got {}",
path, expected_calls, calls_deleted
);
deleted_entity_ids.sort_unstable();
deleted_entity_ids.dedup();
chunks_deleted = graph.chunks.delete_chunks_for_file(path)?;
let normalized_path = crate::graph::files::normalize_path_for_index(path);
let file_id_for_ast = graph
.files
.file_index
.get(&normalized_path)
.map(|id| id.as_i64())
.unwrap_or(0);
ast_nodes_deleted = if file_id_for_ast > 0 {
graph
.side_tables
.delete_ast_nodes_for_file(file_id_for_ast)?
} else {
0
};
assert_eq!(
ast_nodes_deleted, expected_ast_nodes,
"AST node deletion count mismatch for '{}': expected {}, got {}",
path, expected_ast_nodes, ast_nodes_deleted
);
let _ = graph.metrics.delete_file_metrics(path);
let normalized_path = crate::graph::files::normalize_path_for_index(path);
graph.files.file_index.remove(&normalized_path);
graph.invalidate_cache(path);
Ok(DeleteResult {
symbols_deleted,
references_deleted,
calls_deleted,
chunks_deleted,
ast_nodes_deleted,
cfg_blocks_deleted,
edges_deleted,
})
} else {
chunks_deleted = graph.side_tables.delete_chunks_for_file(path)?;
assert_eq!(
chunks_deleted, expected_chunks,
"Code chunk deletion count mismatch (no file) for '{}': expected {}, got {}",
path, expected_chunks, chunks_deleted
);
references_deleted = graph.references.delete_references_in_file(path)?;
assert_eq!(
references_deleted, expected_references,
"Reference deletion count mismatch (no file) for '{}': expected {}, got {}",
path, expected_references, references_deleted
);
calls_deleted = graph.calls.delete_calls_in_file(path)?;
assert_eq!(
calls_deleted, expected_calls,
"Call deletion count mismatch (no file) for '{}': expected {}, got {}",
path, expected_calls, calls_deleted
);
let normalized_path = crate::graph::files::normalize_path_for_index(path);
let file_id_for_ast = graph
.files
.file_index
.get(&normalized_path)
.map(|id| id.as_i64())
.unwrap_or(0);
ast_nodes_deleted = if file_id_for_ast > 0 {
graph
.side_tables
.delete_ast_nodes_for_file(file_id_for_ast)?
} else {
0
};
let _ = graph.metrics.delete_file_metrics(path);
cfg_blocks_deleted = 0;
edges_deleted = 0;
graph.invalidate_cache(path);
Ok(DeleteResult {
symbols_deleted: 0,
references_deleted,
calls_deleted,
chunks_deleted,
ast_nodes_deleted,
cfg_blocks_deleted,
edges_deleted,
})
}
}
fn count_references_in_file(graph: &CodeGraph, path: &str) -> usize {
let snapshot = SnapshotId::current();
graph
.references
.backend
.entity_ids()
.map(|ids| {
ids.iter()
.filter_map(|id| graph.references.backend.get_node(snapshot, *id).ok())
.filter(|node| {
node.kind == "Reference"
&& node
.data
.get("file")
.and_then(|v| v.as_str())
.map(|f| f == path)
.unwrap_or(false)
})
.count()
})
.unwrap_or(0)
}
fn count_calls_in_file(graph: &CodeGraph, path: &str) -> usize {
let snapshot = SnapshotId::current();
graph
.calls
.backend
.entity_ids()
.map(|ids| {
ids.iter()
.filter_map(|id| graph.calls.backend.get_node(snapshot, *id).ok())
.filter(|node| {
node.kind == "Call"
&& node
.data
.get("file")
.and_then(|v| v.as_str())
.map(|f| f == path)
.unwrap_or(false)
})
.count()
})
.unwrap_or(0)
}
pub mod test_helpers {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailPoint {
AfterSymbolsDeleted,
AfterReferencesDeleted,
AfterCallsDeleted,
AfterChunksDeleted,
BeforeFileDeleted,
}
pub fn delete_file_facts_with_injection(
graph: &mut CodeGraph,
path: &str,
verify_at: Option<FailPoint>,
) -> Result<DeleteResult> {
let mut deleted_entity_ids: Vec<i64> = Vec::new();
let symbols_deleted: usize;
let chunks_deleted: usize;
let references_deleted: usize;
let calls_deleted: usize;
let _ast_nodes_deleted: usize = 0; let _cfg_blocks_deleted: usize = 0;
if let Some(file_id) = graph.files.find_file_node(path)? {
let snapshot = SnapshotId::current();
let symbol_ids = graph.files.backend.neighbors(
snapshot,
file_id.as_i64(),
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Outgoing,
edge_type: Some("DEFINES".to_string()),
},
)?;
let mut symbol_ids_sorted = symbol_ids;
symbol_ids_sorted.sort_unstable();
for symbol_id in &symbol_ids_sorted {
graph.files.backend.delete_entity(*symbol_id)?;
}
symbols_deleted = symbol_ids_sorted.len();
deleted_entity_ids.extend(symbol_ids_sorted.iter().copied());
if verify_at == Some(FailPoint::AfterSymbolsDeleted) {
return Ok(DeleteResult {
symbols_deleted,
references_deleted: 0,
calls_deleted: 0,
chunks_deleted: 0,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted: 0,
});
}
graph.files.backend.delete_entity(file_id.as_i64())?;
deleted_entity_ids.push(file_id.as_i64());
let normalized_path = crate::graph::files::normalize_path_for_index(path);
graph.files.file_index.remove(&normalized_path);
references_deleted = graph.references.delete_references_in_file(path)?;
if verify_at == Some(FailPoint::AfterReferencesDeleted) {
return Ok(DeleteResult {
symbols_deleted,
references_deleted,
calls_deleted: 0,
chunks_deleted: 0,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted: 0,
});
}
calls_deleted = graph.calls.delete_calls_in_file(path)?;
if verify_at == Some(FailPoint::AfterCallsDeleted) {
return Ok(DeleteResult {
symbols_deleted,
references_deleted,
calls_deleted,
chunks_deleted: 0,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted: 0,
});
}
if verify_at == Some(FailPoint::BeforeFileDeleted) {
return Ok(DeleteResult {
symbols_deleted,
references_deleted,
calls_deleted,
chunks_deleted: 0,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted: 0,
});
}
deleted_entity_ids.sort_unstable();
deleted_entity_ids.dedup();
let _ = deleted_entity_ids;
let edges_deleted = 0;
chunks_deleted = graph.side_tables.delete_chunks_for_file(path)?;
let normalized_path = crate::graph::files::normalize_path_for_index(path);
graph.files.file_index.remove(&normalized_path);
graph.invalidate_cache(path);
if verify_at == Some(FailPoint::AfterChunksDeleted) {
return Ok(DeleteResult {
symbols_deleted,
references_deleted,
calls_deleted,
chunks_deleted,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted,
});
}
Ok(DeleteResult {
symbols_deleted,
references_deleted,
calls_deleted,
chunks_deleted,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted,
})
} else {
chunks_deleted = graph.side_tables.delete_chunks_for_file(path)?;
references_deleted = graph.references.delete_references_in_file(path)?;
calls_deleted = graph.calls.delete_calls_in_file(path)?;
graph.invalidate_cache(path);
Ok(DeleteResult {
symbols_deleted: 0,
references_deleted,
calls_deleted,
chunks_deleted,
ast_nodes_deleted: 0,
cfg_blocks_deleted: 0,
edges_deleted: 0,
})
}
}
}
fn count_chunks_for_file(graph: &CodeGraph, path: &str) -> usize {
graph.chunks.count_chunks_for_file(path).unwrap_or(0)
}
pub fn insert_ast_nodes(
graph: &mut CodeGraph,
file_id: i64,
nodes: Vec<crate::graph::AstNode>,
) -> Result<usize> {
if nodes.is_empty() {
return Ok(0);
}
let mut nodes_with_ids: Vec<(crate::graph::AstNode, i64)> = Vec::with_capacity(nodes.len());
let mut next_placeholder_id: i64 = -1;
for node in nodes {
let mut node = node;
if let Some(parent_id) = node.parent_id {
if parent_id < 0 {
}
}
if node.id.is_none() {
node.id = Some(next_placeholder_id);
next_placeholder_id -= 1;
}
nodes_with_ids.push((node, file_id));
}
let inserted_ids = graph.side_tables.store_ast_nodes_batch(&nodes_with_ids)?;
for (idx, (original_node, _)) in nodes_with_ids.iter().enumerate() {
if let Some(parent_id) = original_node.parent_id {
if parent_id < 0 {
let parent_index = ((-parent_id) as usize).saturating_sub(1);
if parent_index < inserted_ids.len() {
let actual_parent_id = inserted_ids[parent_index];
let node_id = inserted_ids[idx];
if let Err(e) = graph
.side_tables
.update_ast_node_parent(node_id, actual_parent_id)
{
eprintln!(
"Warning: failed to update parent link for node {}: {:?}",
node_id, e
);
}
}
}
}
}
Ok(nodes_with_ids.len())
}
fn count_ast_nodes_for_file(graph: &CodeGraph, path: &str) -> usize {
let normalized_path = crate::graph::files::normalize_path_for_index(path);
let file_id = match graph.files.file_index.get(&normalized_path) {
Some(id) => id.as_i64(),
None => return 0, };
graph
.side_tables
.count_ast_nodes_for_file(file_id)
.unwrap_or(0)
}
fn count_cfg_blocks_for_file(graph: &CodeGraph, path: &str) -> usize {
use rusqlite::params;
match graph.chunks.connect() {
Ok(conn) => conn
.query_row(
"SELECT COUNT(*) FROM cfg_blocks c
JOIN graph_entities e ON c.function_id = e.id
WHERE e.file_path = ?1",
params![path],
|row| row.get(0),
)
.unwrap_or(0),
Err(_) => 0,
}
}
pub fn reconcile_file_path(
graph: &mut CodeGraph,
path: &Path,
path_key: &str,
) -> Result<ReconcileOutcome> {
use std::fs;
if !path.exists() {
#[cfg(debug_assertions)]
{
let deleted = delete_file_facts(graph, path_key)?;
if !deleted.is_empty() {
eprintln!(
"Deleted {} symbols, {} references, {} calls for missing file {}",
deleted.symbols_deleted,
deleted.references_deleted,
deleted.calls_deleted,
path_key
);
}
}
#[cfg(not(debug_assertions))]
{
let _ = delete_file_facts(graph, path_key)?;
}
return Ok(ReconcileOutcome::Deleted);
}
let source = fs::read(path)?;
let new_hash = graph.files.compute_hash(&source);
let snapshot = SnapshotId::current();
let unchanged = if let Some(file_id) = graph.files.find_file_node(path_key)? {
match graph.files.backend.get_node(snapshot, file_id.as_i64()) {
Ok(node) => {
let file_node: crate::graph::schema::FileNode = serde_json::from_value(node.data)
.unwrap_or_else(|_| crate::graph::schema::FileNode {
path: path_key.to_string(),
hash: String::new(),
last_indexed_at: 0,
last_modified: 0,
});
file_node.hash == new_hash
}
Err(sqlitegraph::SqliteGraphError::NotFound(_)) => {
let normalized_path = crate::graph::files::normalize_path_for_index(path_key);
graph.files.file_index.remove(&normalized_path);
false }
Err(e) => return Err(e.into()),
}
} else {
false };
if unchanged {
return Ok(ReconcileOutcome::Unchanged);
}
#[cfg(debug_assertions)]
{
let deleted = delete_file_facts(graph, path_key)?;
if !deleted.is_empty() {
eprintln!(
"Deleted {} symbols, {} references, {} calls for reindex of {}",
deleted.symbols_deleted,
deleted.references_deleted,
deleted.calls_deleted,
path_key
);
}
}
#[cfg(not(debug_assertions))]
{
let _ = delete_file_facts(graph, path_key)?;
}
let _ = graph.module_resolver.build_module_index();
let symbols = index_file(graph, path_key, &source)?;
query::index_references(graph, path_key, &source)?;
let calls = count_calls_in_file(graph, path_key);
let references = count_references_in_file(graph, path_key);
Ok(ReconcileOutcome::Reindexed {
symbols,
references,
calls,
})
}
pub fn reconcile_file_path_with_source(
graph: &mut CodeGraph,
path: &Path,
path_key: &str,
source: &[u8],
) -> Result<ReconcileOutcome> {
if !path.exists() {
#[cfg(debug_assertions)]
{
let deleted = delete_file_facts(graph, path_key)?;
if !deleted.is_empty() {
eprintln!(
"Deleted {} symbols, {} references, {} calls for missing file {}",
deleted.symbols_deleted,
deleted.references_deleted,
deleted.calls_deleted,
path_key
);
}
}
#[cfg(not(debug_assertions))]
{
let _ = delete_file_facts(graph, path_key)?;
}
return Ok(ReconcileOutcome::Deleted);
}
let new_hash = graph.files.compute_hash(source);
let snapshot = SnapshotId::current();
let unchanged = if let Some(file_id) = graph.files.find_file_node(path_key)? {
match graph.files.backend.get_node(snapshot, file_id.as_i64()) {
Ok(node) => {
let file_node: crate::graph::schema::FileNode = serde_json::from_value(node.data)
.unwrap_or_else(|_| crate::graph::schema::FileNode {
path: path_key.to_string(),
hash: String::new(),
last_indexed_at: 0,
last_modified: 0,
});
file_node.hash == new_hash
}
Err(sqlitegraph::SqliteGraphError::NotFound(_)) => {
let normalized_path = crate::graph::files::normalize_path_for_index(path_key);
graph.files.file_index.remove(&normalized_path);
false
}
Err(e) => return Err(e.into()),
}
} else {
false
};
if unchanged {
return Ok(ReconcileOutcome::Unchanged);
}
#[cfg(debug_assertions)]
{
let deleted = delete_file_facts(graph, path_key)?;
if !deleted.is_empty() {
eprintln!(
"Deleted {} symbols, {} references, {} calls for reindex of {}",
deleted.symbols_deleted,
deleted.references_deleted,
deleted.calls_deleted,
path_key
);
}
}
#[cfg(not(debug_assertions))]
{
let _ = delete_file_facts(graph, path_key)?;
}
let _ = graph.module_resolver.build_module_index();
let symbols = index_file(graph, path_key, source)?;
query::index_references(graph, path_key, source)?;
let calls = count_calls_in_file(graph, path_key);
let references = count_references_in_file(graph, path_key);
Ok(ReconcileOutcome::Reindexed {
symbols,
references,
calls,
})
}
#[cfg(test)]
mod tests {
#[test]
fn test_ast_nodes_indexed_with_file() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let mut graph = crate::CodeGraph::open(&db_path).unwrap();
let source = b"fn main() { if true { println!(\"hello\"); } }";
graph.index_file("test.rs", source).unwrap();
let conn = graph.chunks.connect().unwrap();
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM ast_nodes", [], |row| row.get(0))
.unwrap();
assert!(count > 0, "AST nodes should be created during indexing");
let if_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM ast_nodes WHERE kind = 'if_expression'",
[],
|row| row.get(0),
)
.unwrap();
assert!(if_count > 0, "if_expression should be indexed");
}
}