1use rusqlite::{Connection, params};
2use std::path::Path;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use crate::error::{Error, Result};
6
7pub struct CacheStore {
9 conn: Connection,
10 ttl_secs: i64,
11}
12
13impl CacheStore {
14 pub fn open(path: &Path) -> Result<Self> {
15 let conn = Connection::open(path)?;
16 conn.execute_batch(
17 "CREATE TABLE IF NOT EXISTS documents (
18 id TEXT PRIMARY KEY,
19 category TEXT NOT NULL,
20 name TEXT NOT NULL,
21 data TEXT NOT NULL,
22 fetched_at INTEGER NOT NULL
23 );
24 CREATE INDEX IF NOT EXISTS idx_cat_name ON documents(category, name);",
25 )?;
26 Ok(Self {
27 conn,
28 ttl_secs: 86400 * 7,
29 })
30 }
31
32 pub fn set_ttl(&mut self, secs: i64) {
33 self.ttl_secs = secs;
34 }
35
36 pub fn get(&self, id: &str) -> Result<Option<String>> {
37 let cutoff = now_epoch() - self.ttl_secs;
38 let mut stmt = self
39 .conn
40 .prepare("SELECT data FROM documents WHERE id = ?1 AND fetched_at > ?2")?;
41 match stmt.query_row(params![id, cutoff], |row| row.get::<_, String>(0)) {
42 Ok(data) => Ok(Some(data)),
43 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
44 Err(e) => Err(Error::Cache(e)),
45 }
46 }
47
48 pub fn get_by_category(&self, category: &str) -> Result<Vec<String>> {
49 let cutoff = now_epoch() - self.ttl_secs;
50 let mut stmt = self
51 .conn
52 .prepare("SELECT data FROM documents WHERE category = ?1 AND fetched_at > ?2")?;
53 let rows = stmt
54 .query_map(params![category, cutoff], |row| row.get::<_, String>(0))?
55 .filter_map(|r| r.ok())
56 .collect();
57 Ok(rows)
58 }
59
60 pub fn put(&self, id: &str, category: &str, name: &str, data: &str) -> Result<()> {
61 self.conn.execute(
62 "INSERT OR REPLACE INTO documents (id, category, name, data, fetched_at)
63 VALUES (?1, ?2, ?3, ?4, ?5)",
64 params![id, category, name, data, now_epoch()],
65 )?;
66 Ok(())
67 }
68
69 pub fn bulk_put(&mut self, docs: &[(String, String, String, String)]) -> Result<usize> {
70 let tx = self.conn.transaction()?;
71 let mut count = 0;
72 for (id, category, name, data) in docs {
73 tx.execute(
74 "INSERT OR REPLACE INTO documents (id, category, name, data, fetched_at)
75 VALUES (?1, ?2, ?3, ?4, ?5)",
76 params![id, category, name, data, now_epoch()],
77 )?;
78 count += 1;
79 }
80 tx.commit()?;
81 Ok(count)
82 }
83
84 pub fn get_by_name(&self, name: &str, category: Option<&str>) -> Result<Vec<String>> {
86 let cutoff = now_epoch() - self.ttl_secs;
87 let (sql, params_vec): (&str, Vec<Box<dyn rusqlite::types::ToSql>>) = match category {
88 Some(cat) => (
89 "SELECT data FROM documents WHERE name = ?1 COLLATE NOCASE AND category = ?2 AND fetched_at > ?3",
90 vec![
91 Box::new(name.to_string()),
92 Box::new(cat.to_string()),
93 Box::new(cutoff),
94 ],
95 ),
96 None => (
97 "SELECT data FROM documents WHERE name = ?1 COLLATE NOCASE AND fetched_at > ?2",
98 vec![Box::new(name.to_string()), Box::new(cutoff)],
99 ),
100 };
101 let mut stmt = self.conn.prepare(sql)?;
102 let params_refs: Vec<&dyn rusqlite::types::ToSql> =
103 params_vec.iter().map(|p| p.as_ref()).collect();
104 let rows = stmt
105 .query_map(params_refs.as_slice(), |row| row.get::<_, String>(0))?
106 .filter_map(|r| r.ok())
107 .collect();
108 Ok(rows)
109 }
110
111 pub fn purge_expired(&self) -> Result<usize> {
113 let cutoff = now_epoch() - self.ttl_secs;
114 let deleted = self.conn.execute(
115 "DELETE FROM documents WHERE fetched_at <= ?1",
116 params![cutoff],
117 )?;
118 Ok(deleted)
119 }
120
121 pub fn status(&self) -> Result<Vec<(String, i64)>> {
122 let mut stmt = self.conn.prepare(
123 "SELECT category, COUNT(*) FROM documents GROUP BY category ORDER BY category",
124 )?;
125 let rows = stmt
126 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
127 .filter_map(|r| r.ok())
128 .collect();
129 Ok(rows)
130 }
131}
132
133fn now_epoch() -> i64 {
134 SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .expect("system clock before UNIX epoch")
137 .as_secs() as i64
138}