1use rusqlite::{params, Connection, OptionalExtension};
8
9const CREATE_GITHUB_READ_CACHE_SCHEMA: &str = r#"
10CREATE TABLE IF NOT EXISTS github_read_cache (
11 resource_kind TEXT NOT NULL CHECK (resource_kind IN ('issue', 'pr')),
12 repository TEXT NOT NULL,
13 resource_number INTEGER NOT NULL CHECK (resource_number > 0),
14 authentication_identity_hash BLOB NOT NULL,
15 canonical_text TEXT NOT NULL,
16 fetched_at_ms INTEGER NOT NULL,
17 updated_at_ms INTEGER NOT NULL,
18 PRIMARY KEY (resource_kind, repository, resource_number, authentication_identity_hash)
19);
20CREATE INDEX IF NOT EXISTS idx_github_read_cache_hard_ttl
21 ON github_read_cache (fetched_at_ms);
22"#;
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum GithubReadResourceKind {
27 Issue,
28 PullRequest,
29}
30
31impl GithubReadResourceKind {
32 pub const fn as_str(self) -> &'static str {
33 match self {
34 Self::Issue => "issue",
35 Self::PullRequest => "pr",
36 }
37 }
38}
39
40#[derive(Clone, Eq, PartialEq)]
45pub struct GithubReadCacheKey {
46 resource_kind: GithubReadResourceKind,
47 normalized_repository: String,
48 resource_number: i64,
49 authentication_identity_hash: [u8; 32],
50}
51
52impl GithubReadCacheKey {
53 pub fn new(
56 resource_kind: GithubReadResourceKind,
57 resolved_repository: &str,
58 resource_number: i64,
59 effective_authentication_identity: &str,
60 ) -> Self {
61 Self {
62 resource_kind,
63 normalized_repository: normalize_repository(resolved_repository),
64 resource_number,
65 authentication_identity_hash: authentication_identity_hash(
66 effective_authentication_identity,
67 ),
68 }
69 }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct GithubReadCacheEntry {
75 pub canonical_text: String,
76 pub fetched_at_ms: i64,
78 pub updated_at_ms: i64,
80}
81
82pub fn ensure_github_read_cache_schema(conn: &Connection) -> rusqlite::Result<()> {
89 conn.execute_batch(CREATE_GITHUB_READ_CACHE_SCHEMA)
90}
91
92pub fn lookup_github_read_cache_entry(
94 conn: &Connection,
95 key: &GithubReadCacheKey,
96) -> rusqlite::Result<Option<GithubReadCacheEntry>> {
97 ensure_github_read_cache_schema(conn)?;
98 conn.query_row(
99 "SELECT canonical_text, fetched_at_ms, updated_at_ms
100 FROM github_read_cache
101 WHERE resource_kind = ?1
102 AND repository = ?2
103 AND resource_number = ?3
104 AND authentication_identity_hash = ?4",
105 params![
106 key.resource_kind.as_str(),
107 &key.normalized_repository,
108 key.resource_number,
109 key.authentication_identity_hash.as_slice(),
110 ],
111 |row| {
112 Ok(GithubReadCacheEntry {
113 canonical_text: row.get(0)?,
114 fetched_at_ms: row.get(1)?,
115 updated_at_ms: row.get(2)?,
116 })
117 },
118 )
119 .optional()
120}
121
122pub fn upsert_github_read_cache_entry(
127 conn: &Connection,
128 key: &GithubReadCacheKey,
129 canonical_text: &str,
130 fetched_at_ms: i64,
131) -> rusqlite::Result<()> {
132 ensure_github_read_cache_schema(conn)?;
133 conn.execute(
134 "INSERT INTO github_read_cache (
135 resource_kind,
136 repository,
137 resource_number,
138 authentication_identity_hash,
139 canonical_text,
140 fetched_at_ms,
141 updated_at_ms
142 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
143 ON CONFLICT(resource_kind, repository, resource_number, authentication_identity_hash)
144 DO UPDATE SET
145 canonical_text = excluded.canonical_text,
146 fetched_at_ms = excluded.fetched_at_ms,
147 updated_at_ms = excluded.updated_at_ms",
148 params![
149 key.resource_kind.as_str(),
150 &key.normalized_repository,
151 key.resource_number,
152 key.authentication_identity_hash.as_slice(),
153 canonical_text,
154 fetched_at_ms,
155 fetched_at_ms,
156 ],
157 )?;
158 Ok(())
159}
160
161pub fn evict_hard_expired_github_read_cache_entries(
166 conn: &Connection,
167 hard_ttl_cutoff_ms: i64,
168) -> rusqlite::Result<usize> {
169 ensure_github_read_cache_schema(conn)?;
170 conn.execute(
171 "DELETE FROM github_read_cache WHERE fetched_at_ms <= ?1",
172 [hard_ttl_cutoff_ms],
173 )
174}
175
176pub fn invalidate_github_read_cache_resource(
181 conn: &Connection,
182 resource_kind: GithubReadResourceKind,
183 resolved_repository: &str,
184 resource_number: i64,
185 effective_authentication_identity: Option<&str>,
186) -> rusqlite::Result<usize> {
187 ensure_github_read_cache_schema(conn)?;
188 let normalized_repository = normalize_repository(resolved_repository);
189
190 match effective_authentication_identity {
191 Some(identity) => conn.execute(
192 "DELETE FROM github_read_cache
193 WHERE resource_kind = ?1
194 AND repository = ?2
195 AND resource_number = ?3
196 AND authentication_identity_hash = ?4",
197 params![
198 resource_kind.as_str(),
199 normalized_repository,
200 resource_number,
201 authentication_identity_hash(identity).as_slice(),
202 ],
203 ),
204 None => conn.execute(
205 "DELETE FROM github_read_cache
206 WHERE resource_kind = ?1 AND repository = ?2 AND resource_number = ?3",
207 params![
208 resource_kind.as_str(),
209 normalized_repository,
210 resource_number
211 ],
212 ),
213 }
214}
215
216fn normalize_repository(repository: &str) -> String {
217 repository.trim().to_ascii_lowercase()
218}
219
220fn authentication_identity_hash(identity: &str) -> [u8; 32] {
221 *blake3::hash(identity.as_bytes()).as_bytes()
222}