use std::path::{Path, PathBuf};
use std::sync::Arc;
use sqry_core::graph::unified::concurrent::GraphSnapshot;
use sqry_core::graph::unified::node::NodeId;
use crate::persistence::{LoadError, LoadOutcome, compute_file_sha256, load_derived};
use crate::queries::{
CalleesQuery, CallersQuery, ExportsQuery, ImportsQuery, ReferencesQuery, RelationKey,
};
use crate::{QueryDb, QueryDbConfig};
#[must_use]
pub fn make_query_db(snapshot: Arc<GraphSnapshot>) -> QueryDb {
QueryDb::new(snapshot, QueryDbConfig::default())
}
#[must_use]
pub fn make_query_db_cold(snapshot: Arc<GraphSnapshot>, workspace_root: &Path) -> QueryDb {
let mut db = QueryDb::new(Arc::clone(&snapshot), QueryDbConfig::default());
let _ = load_derived_opportunistic(&mut db, workspace_root);
db
}
#[must_use]
pub fn derived_path(workspace_root: &Path, config: &QueryDbConfig) -> PathBuf {
workspace_root
.join(".sqry")
.join("graph")
.join(&config.derived_persistence_filename)
}
pub fn load_derived_opportunistic(
db: &mut QueryDb,
workspace_root: &Path,
) -> Result<LoadOutcome, LoadError> {
let config = db.config().clone();
let snapshot_path = workspace_root
.join(".sqry")
.join("graph")
.join("snapshot.sqry");
let derived = derived_path(workspace_root, &config);
let snapshot_sha256 = match compute_file_sha256(&snapshot_path) {
Ok(sha) => sha,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(LoadError::NotFound {
path: snapshot_path,
});
}
Err(e) => {
log::warn!("PN3 cold load: failed to hash snapshot file: {e}");
return Err(LoadError::Io(e));
}
};
match load_derived(db, snapshot_sha256, &derived, workspace_root) {
Ok(outcome) => {
match &outcome {
LoadOutcome::Applied { entries } => {
log::debug!(
"PN3 cold load: {entries} entries applied from {}",
derived.display()
);
}
LoadOutcome::Skipped(reason) => {
log::debug!("PN3 cold load skipped: {reason:?}");
}
}
Ok(outcome)
}
Err(LoadError::NotFound { .. }) => {
Err(LoadError::NotFound { path: derived })
}
Err(err @ (LoadError::StaleSnapshot | LoadError::Corrupt { .. })) => {
let _ = std::fs::remove_file(&derived);
log::info!("PN3 discarded stale/corrupt derived-cache file: {err}");
Err(err)
}
Err(LoadError::AlreadyLoaded) => {
log::debug!("PN3 cold load: AlreadyLoaded (unexpected for fresh DB)");
Err(LoadError::AlreadyLoaded)
}
Err(err) => {
log::warn!("PN3 cold load failed: {err}");
Err(err)
}
}
}
#[must_use]
pub fn mcp_callers_query(db: &QueryDb, key: &RelationKey) -> Arc<Vec<NodeId>> {
db.get::<CalleesQuery>(key)
}
#[must_use]
pub fn mcp_callees_query(db: &QueryDb, key: &RelationKey) -> Arc<Vec<NodeId>> {
db.get::<CallersQuery>(key)
}
#[must_use]
pub fn mcp_imports_query(db: &QueryDb, key: &RelationKey) -> Arc<Vec<NodeId>> {
db.get::<ImportsQuery>(key)
}
#[must_use]
pub fn mcp_exports_query(db: &QueryDb, key: &RelationKey) -> Arc<Vec<NodeId>> {
db.get::<ExportsQuery>(key)
}
#[must_use]
pub fn mcp_references_query(db: &QueryDb, key: &RelationKey) -> Arc<Vec<NodeId>> {
db.get::<ReferencesQuery>(key)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use std::sync::Arc;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::node::NodeKind;
use sqry_core::graph::unified::storage::NodeEntry;
fn build_caller_graph() -> (Arc<GraphSnapshot>, NodeId, NodeId, NodeId) {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("lib.rs")).unwrap();
let main_name = graph.strings_mut().intern("main").unwrap();
let helper_name = graph.strings_mut().intern("helper").unwrap();
let isolated_name = graph.strings_mut().intern("isolated").unwrap();
let main_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, main_name, file_id)
.with_qualified_name(main_name),
)
.unwrap();
let helper_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, helper_name, file_id)
.with_qualified_name(helper_name),
)
.unwrap();
let isolated_id = graph
.nodes_mut()
.alloc(
NodeEntry::new(NodeKind::Function, isolated_name, file_id)
.with_qualified_name(isolated_name),
)
.unwrap();
graph.edges_mut().add_edge(
main_id,
helper_id,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
(Arc::new(graph.snapshot()), main_id, helper_id, isolated_id)
}
#[test]
fn mcp_callers_query_returns_callers_of_symbol_under_graph_eval_convention() {
let (snapshot, main_id, helper_id, isolated_id) = build_caller_graph();
let db = make_query_db(snapshot);
let key = RelationKey::exact("helper");
let callers_of_helper = mcp_callers_query(&db, &key);
assert!(
callers_of_helper.contains(&main_id),
"main is a caller of helper"
);
assert!(
!callers_of_helper.contains(&helper_id),
"helper does not call itself"
);
assert!(
!callers_of_helper.contains(&isolated_id),
"isolated has no calls"
);
}
#[test]
fn mcp_callees_query_returns_callees_of_symbol_under_graph_eval_convention() {
let (snapshot, main_id, helper_id, isolated_id) = build_caller_graph();
let db = make_query_db(snapshot);
let key = RelationKey::exact("main");
let callees_of_main = mcp_callees_query(&db, &key);
assert!(
callees_of_main.contains(&helper_id),
"helper is a callee of main"
);
assert!(
!callees_of_main.contains(&main_id),
"main does not call itself"
);
assert!(
!callees_of_main.contains(&isolated_id),
"isolated is not called by main"
);
}
use tempfile::TempDir;
fn setup_workspace() -> (TempDir, std::path::PathBuf, [u8; 32]) {
let dir = TempDir::new().unwrap();
let workspace_root = dir.path().to_path_buf();
let graph_dir = workspace_root.join(".sqry").join("graph");
std::fs::create_dir_all(&graph_dir).unwrap();
let snapshot_bytes = b"fake_snapshot_content_for_test";
let snapshot_path = graph_dir.join("snapshot.sqry");
std::fs::write(&snapshot_path, snapshot_bytes).unwrap();
let sha = crate::persistence::compute_file_sha256(&snapshot_path).unwrap();
(dir, workspace_root, sha)
}
fn write_derived_file(workspace_root: &std::path::Path, sha: [u8; 32], n_entries: usize) {
use crate::persistence::{
DerivedHeader, PersistedEntry, QueryDeps, serialize_derived_stream,
};
use crate::queries::type_ids;
let entries: Vec<PersistedEntry> = (0..n_entries)
.map(|i| PersistedEntry {
query_type_id: type_ids::CALLERS,
raw_key_bytes: vec![i as u8],
raw_result_bytes: vec![0xAA, i as u8],
deps: QueryDeps::default(),
})
.collect();
let header = DerivedHeader::new(sha, 0, 0, vec![], entries.len() as u64);
let bytes = serialize_derived_stream(&header, entries).unwrap();
let derived_path = workspace_root
.join(".sqry")
.join("graph")
.join("derived.sqry");
std::fs::write(&derived_path, bytes).unwrap();
}
#[test]
fn make_query_db_cold_on_empty_workspace_does_not_create_derived_file() {
let (dir, workspace_root, _sha) = setup_workspace();
let derived_path = workspace_root
.join(".sqry")
.join("graph")
.join("derived.sqry");
assert!(
!derived_path.exists(),
"pre-condition: derived.sqry must not exist"
);
let (snapshot, _, _, _) = build_caller_graph();
let db = make_query_db_cold(snapshot, &workspace_root);
assert!(
db.cold_load_allowed(),
"cold_load_allowed must remain true: no load succeeded"
);
assert!(
!derived_path.exists(),
"make_query_db_cold must NOT create derived.sqry"
);
drop(dir);
}
#[test]
fn make_query_db_cold_with_stale_sha_deletes_file_and_returns_pristine_db() {
let (dir, workspace_root, sha) = setup_workspace();
write_derived_file(&workspace_root, sha, 2);
let derived_path = workspace_root
.join(".sqry")
.join("graph")
.join("derived.sqry");
assert!(
derived_path.exists(),
"pre-condition: derived.sqry must exist"
);
let snapshot_path = workspace_root
.join(".sqry")
.join("graph")
.join("snapshot.sqry");
std::fs::write(&snapshot_path, b"different_snapshot_bytes").unwrap();
let (snapshot, _, _, _) = build_caller_graph();
let db = make_query_db_cold(snapshot, &workspace_root);
assert!(
db.cold_load_allowed(),
"cold_load_allowed must remain true: stale load was rejected"
);
assert!(
!derived_path.exists(),
"make_query_db_cold must delete stale derived.sqry"
);
drop(dir);
}
#[test]
fn make_query_db_cold_with_matching_sha_cold_loads() {
let (dir, workspace_root, sha) = setup_workspace();
write_derived_file(&workspace_root, sha, 3);
let (snapshot, _, _, _) = build_caller_graph();
let db = make_query_db_cold(snapshot, &workspace_root);
assert!(
!db.cold_load_allowed(),
"cold_load_allowed must be false after a successful load"
);
drop(dir);
}
}