lean_ctx/core/property_graph/
semantic_cache.rs1use rusqlite::{Connection, OptionalExtension, params};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CachedResolution {
14 pub backend: String,
16 pub outcome: String,
19 pub target_file: Option<String>,
20 pub target_line: Option<usize>,
21 pub target_symbol: Option<String>,
22 pub context: String,
26}
27
28pub type SiteKey<'a> = (&'a str, usize, usize, &'a str);
30
31pub(super) fn lookup(
32 conn: &Connection,
33 site: SiteKey<'_>,
34 caller_hash: &str,
35) -> anyhow::Result<Option<CachedResolution>> {
36 let (file, line, col, op) = site;
37 let row = conn
38 .query_row(
39 "SELECT backend, outcome, target_file, target_line, target_symbol, context
40 FROM semantic_resolutions
41 WHERE caller_file = ?1 AND line = ?2 AND col = ?3 AND op = ?4
42 AND caller_hash = ?5",
43 params![file, line as i64, col as i64, op, caller_hash],
44 |r| {
45 Ok(CachedResolution {
46 backend: r.get(0)?,
47 outcome: r.get(1)?,
48 target_file: r.get(2)?,
49 target_line: r.get::<_, Option<i64>>(3)?.map(|l| l as usize),
50 target_symbol: r.get(4)?,
51 context: r.get(5)?,
52 })
53 },
54 )
55 .optional()?;
56 Ok(row)
57}
58
59pub fn lookup_served(
71 conn: &Connection,
72 site: SiteKey<'_>,
73 caller_hash: &str,
74 mut serves: impl FnMut(&str) -> bool,
75) -> anyhow::Result<Option<CachedResolution>> {
76 Ok(lookup(conn, site, caller_hash)?.filter(|row| {
77 serves(site.0)
78 && row
79 .target_file
80 .as_deref()
81 .is_none_or(|targets| targets.lines().all(&mut serves))
82 }))
83}
84
85pub(super) fn store(
86 conn: &Connection,
87 site: SiteKey<'_>,
88 caller_hash: &str,
89 value: &CachedResolution,
90) -> anyhow::Result<()> {
91 let (file, line, col, op) = site;
92 conn.execute(
93 "INSERT INTO semantic_resolutions
94 (caller_file, line, col, op, caller_hash, backend, outcome,
95 target_file, target_line, target_symbol, context)
96 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
97 ON CONFLICT(caller_file, line, col, op) DO UPDATE SET
98 caller_hash = excluded.caller_hash,
99 backend = excluded.backend,
100 outcome = excluded.outcome,
101 target_file = excluded.target_file,
102 target_line = excluded.target_line,
103 target_symbol = excluded.target_symbol,
104 context = excluded.context",
105 params![
106 file,
107 line as i64,
108 col as i64,
109 op,
110 caller_hash,
111 value.backend,
112 value.outcome,
113 value.target_file,
114 value.target_line.map(|l| l as i64),
115 value.target_symbol,
116 value.context,
117 ],
118 )?;
119 Ok(())
120}
121
122pub fn store_admitted(
129 conn: &Connection,
130 site: SiteKey<'_>,
131 caller_hash: &str,
132 value: &CachedResolution,
133 admission: &crate::core::context_admission::stores::StoreAdmission,
134) -> anyhow::Result<bool> {
135 let kept = [
136 Some(site.0),
137 Some(value.backend.as_str()),
138 Some(value.outcome.as_str()),
139 value.target_file.as_deref(),
140 value.target_symbol.as_deref(),
141 ]
142 .into_iter()
143 .flatten()
144 .collect::<Vec<_>>()
145 .join("\n");
146 if admission.admit_instance_text(&kept).as_deref() != Some(kept.as_str()) {
147 return Ok(false);
148 }
149 store(conn, site, caller_hash, value)?;
150 Ok(true)
151}
152
153pub(super) fn any(conn: &Connection) -> anyhow::Result<bool> {
156 Ok(conn.query_row(
157 "SELECT EXISTS(SELECT 1 FROM semantic_resolutions)",
158 [],
159 |r| r.get(0),
160 )?)
161}
162
163pub(super) fn prune(
166 conn: &Connection,
167 live_hashes: &std::collections::HashMap<String, String>,
168) -> anyhow::Result<usize> {
169 let mut stmt =
170 conn.prepare("SELECT DISTINCT caller_file, caller_hash FROM semantic_resolutions")?;
171 let stale: Vec<(String, String)> = stmt
172 .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?
173 .filter_map(Result::ok)
174 .filter(|(file, hash)| live_hashes.get(file) != Some(hash))
175 .collect();
176 let mut removed = 0;
177 for (file, hash) in stale {
178 removed += conn.execute(
179 "DELETE FROM semantic_resolutions WHERE caller_file = ?1 AND caller_hash = ?2",
180 params![file, hash],
181 )?;
182 }
183 Ok(removed)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::core::property_graph::CodeGraph;
190
191 #[test]
195 fn a_cached_answer_is_used_only_while_its_files_are_served() {
196 let _data = crate::core::data_dir::isolated_data_dir();
197 let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
198 let project = tempfile::tempdir().expect("project");
199 let root = project.path().canonicalize().expect("canonical root");
200 let root_text = root.to_str().expect("utf-8 root");
201 for name in ["app.rs", "billing.rs"] {
202 std::fs::write(root.join(name), "pub fn settle() {}\n").expect("source");
203 }
204 let site: SiteKey<'_> = ("app.rs", 2, 4, "definition");
205 let answer = CachedResolution {
206 backend: "lsp:test".to_owned(),
207 outcome: "resolved".to_owned(),
208 target_file: Some("billing.rs".to_owned()),
209 target_line: Some(1),
210 target_symbol: Some("settle".to_owned()),
211 context: "c".to_owned(),
212 };
213 let used = || {
215 let graph = CodeGraph::open(root_text).expect("graph");
216 lookup_served(&graph.conn, site, "h", |file| graph.serves_file(file)).expect("query")
217 };
218 let graph = CodeGraph::open(root_text).expect("graph");
219 store(&graph.conn, site, "h", &answer).expect("stored");
220 assert_eq!(used(), Some(answer.clone()));
221
222 std::fs::remove_file(root.join("billing.rs")).expect("withdraw");
223 assert_eq!(used(), None);
224 assert_eq!(lookup(&graph.conn, site, "h").expect("row"), Some(answer));
225 }
226}