use rusqlite::{Connection, OptionalExtension, params};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CachedResolution {
pub backend: String,
pub outcome: String,
pub target_file: Option<String>,
pub target_line: Option<usize>,
pub target_symbol: Option<String>,
pub context: String,
}
pub type SiteKey<'a> = (&'a str, usize, usize, &'a str);
pub(super) fn lookup(
conn: &Connection,
site: SiteKey<'_>,
caller_hash: &str,
) -> anyhow::Result<Option<CachedResolution>> {
let (file, line, col, op) = site;
let row = conn
.query_row(
"SELECT backend, outcome, target_file, target_line, target_symbol, context
FROM semantic_resolutions
WHERE caller_file = ?1 AND line = ?2 AND col = ?3 AND op = ?4
AND caller_hash = ?5",
params![file, line as i64, col as i64, op, caller_hash],
|r| {
Ok(CachedResolution {
backend: r.get(0)?,
outcome: r.get(1)?,
target_file: r.get(2)?,
target_line: r.get::<_, Option<i64>>(3)?.map(|l| l as usize),
target_symbol: r.get(4)?,
context: r.get(5)?,
})
},
)
.optional()?;
Ok(row)
}
pub fn lookup_served(
conn: &Connection,
site: SiteKey<'_>,
caller_hash: &str,
mut serves: impl FnMut(&str) -> bool,
) -> anyhow::Result<Option<CachedResolution>> {
Ok(lookup(conn, site, caller_hash)?.filter(|row| {
serves(site.0)
&& row
.target_file
.as_deref()
.is_none_or(|targets| targets.lines().all(&mut serves))
}))
}
pub(super) fn store(
conn: &Connection,
site: SiteKey<'_>,
caller_hash: &str,
value: &CachedResolution,
) -> anyhow::Result<()> {
let (file, line, col, op) = site;
conn.execute(
"INSERT INTO semantic_resolutions
(caller_file, line, col, op, caller_hash, backend, outcome,
target_file, target_line, target_symbol, context)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
ON CONFLICT(caller_file, line, col, op) DO UPDATE SET
caller_hash = excluded.caller_hash,
backend = excluded.backend,
outcome = excluded.outcome,
target_file = excluded.target_file,
target_line = excluded.target_line,
target_symbol = excluded.target_symbol,
context = excluded.context",
params![
file,
line as i64,
col as i64,
op,
caller_hash,
value.backend,
value.outcome,
value.target_file,
value.target_line.map(|l| l as i64),
value.target_symbol,
value.context,
],
)?;
Ok(())
}
pub fn store_admitted(
conn: &Connection,
site: SiteKey<'_>,
caller_hash: &str,
value: &CachedResolution,
admission: &crate::core::context_admission::stores::StoreAdmission,
) -> anyhow::Result<bool> {
let kept = [
Some(site.0),
Some(value.backend.as_str()),
Some(value.outcome.as_str()),
value.target_file.as_deref(),
value.target_symbol.as_deref(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("\n");
if admission.admit_instance_text(&kept).as_deref() != Some(kept.as_str()) {
return Ok(false);
}
store(conn, site, caller_hash, value)?;
Ok(true)
}
pub(super) fn any(conn: &Connection) -> anyhow::Result<bool> {
Ok(conn.query_row(
"SELECT EXISTS(SELECT 1 FROM semantic_resolutions)",
[],
|r| r.get(0),
)?)
}
pub(super) fn prune(
conn: &Connection,
live_hashes: &std::collections::HashMap<String, String>,
) -> anyhow::Result<usize> {
let mut stmt =
conn.prepare("SELECT DISTINCT caller_file, caller_hash FROM semantic_resolutions")?;
let stale: Vec<(String, String)> = stmt
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?
.filter_map(Result::ok)
.filter(|(file, hash)| live_hashes.get(file) != Some(hash))
.collect();
let mut removed = 0;
for (file, hash) in stale {
removed += conn.execute(
"DELETE FROM semantic_resolutions WHERE caller_file = ?1 AND caller_hash = ?2",
params![file, hash],
)?;
}
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::property_graph::CodeGraph;
#[test]
fn a_cached_answer_is_used_only_while_its_files_are_served() {
let _data = crate::core::data_dir::isolated_data_dir();
let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project");
let root = project.path().canonicalize().expect("canonical root");
let root_text = root.to_str().expect("utf-8 root");
for name in ["app.rs", "billing.rs"] {
std::fs::write(root.join(name), "pub fn settle() {}\n").expect("source");
}
let site: SiteKey<'_> = ("app.rs", 2, 4, "definition");
let answer = CachedResolution {
backend: "lsp:test".to_owned(),
outcome: "resolved".to_owned(),
target_file: Some("billing.rs".to_owned()),
target_line: Some(1),
target_symbol: Some("settle".to_owned()),
context: "c".to_owned(),
};
let used = || {
let graph = CodeGraph::open(root_text).expect("graph");
lookup_served(&graph.conn, site, "h", |file| graph.serves_file(file)).expect("query")
};
let graph = CodeGraph::open(root_text).expect("graph");
store(&graph.conn, site, "h", &answer).expect("stored");
assert_eq!(used(), Some(answer.clone()));
std::fs::remove_file(root.join("billing.rs")).expect("withdraw");
assert_eq!(used(), None);
assert_eq!(lookup(&graph.conn, site, "h").expect("row"), Some(answer));
}
}