use rusqlite::{params, Connection, OptionalExtension};
const CREATE_GITHUB_READ_CACHE_SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS github_read_cache (
resource_kind TEXT NOT NULL CHECK (resource_kind IN ('issue', 'pr')),
repository TEXT NOT NULL,
resource_number INTEGER NOT NULL CHECK (resource_number > 0),
authentication_identity_hash BLOB NOT NULL,
canonical_text TEXT NOT NULL,
fetched_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (resource_kind, repository, resource_number, authentication_identity_hash)
);
CREATE INDEX IF NOT EXISTS idx_github_read_cache_hard_ttl
ON github_read_cache (fetched_at_ms);
"#;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GithubReadResourceKind {
Issue,
PullRequest,
}
impl GithubReadResourceKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Issue => "issue",
Self::PullRequest => "pr",
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct GithubReadCacheKey {
resource_kind: GithubReadResourceKind,
normalized_repository: String,
resource_number: i64,
authentication_identity_hash: [u8; 32],
}
impl GithubReadCacheKey {
pub fn new(
resource_kind: GithubReadResourceKind,
resolved_repository: &str,
resource_number: i64,
effective_authentication_identity: &str,
) -> Self {
Self {
resource_kind,
normalized_repository: normalize_repository(resolved_repository),
resource_number,
authentication_identity_hash: authentication_identity_hash(
effective_authentication_identity,
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GithubReadCacheEntry {
pub canonical_text: String,
pub fetched_at_ms: i64,
pub updated_at_ms: i64,
}
pub fn ensure_github_read_cache_schema(conn: &Connection) -> rusqlite::Result<()> {
conn.execute_batch(CREATE_GITHUB_READ_CACHE_SCHEMA)
}
pub fn lookup_github_read_cache_entry(
conn: &Connection,
key: &GithubReadCacheKey,
) -> rusqlite::Result<Option<GithubReadCacheEntry>> {
ensure_github_read_cache_schema(conn)?;
conn.query_row(
"SELECT canonical_text, fetched_at_ms, updated_at_ms
FROM github_read_cache
WHERE resource_kind = ?1
AND repository = ?2
AND resource_number = ?3
AND authentication_identity_hash = ?4",
params![
key.resource_kind.as_str(),
&key.normalized_repository,
key.resource_number,
key.authentication_identity_hash.as_slice(),
],
|row| {
Ok(GithubReadCacheEntry {
canonical_text: row.get(0)?,
fetched_at_ms: row.get(1)?,
updated_at_ms: row.get(2)?,
})
},
)
.optional()
}
pub fn upsert_github_read_cache_entry(
conn: &Connection,
key: &GithubReadCacheKey,
canonical_text: &str,
fetched_at_ms: i64,
) -> rusqlite::Result<()> {
ensure_github_read_cache_schema(conn)?;
conn.execute(
"INSERT INTO github_read_cache (
resource_kind,
repository,
resource_number,
authentication_identity_hash,
canonical_text,
fetched_at_ms,
updated_at_ms
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(resource_kind, repository, resource_number, authentication_identity_hash)
DO UPDATE SET
canonical_text = excluded.canonical_text,
fetched_at_ms = excluded.fetched_at_ms,
updated_at_ms = excluded.updated_at_ms",
params![
key.resource_kind.as_str(),
&key.normalized_repository,
key.resource_number,
key.authentication_identity_hash.as_slice(),
canonical_text,
fetched_at_ms,
fetched_at_ms,
],
)?;
Ok(())
}
pub fn evict_hard_expired_github_read_cache_entries(
conn: &Connection,
hard_ttl_cutoff_ms: i64,
) -> rusqlite::Result<usize> {
ensure_github_read_cache_schema(conn)?;
conn.execute(
"DELETE FROM github_read_cache WHERE fetched_at_ms <= ?1",
[hard_ttl_cutoff_ms],
)
}
pub fn invalidate_github_read_cache_resource(
conn: &Connection,
resource_kind: GithubReadResourceKind,
resolved_repository: &str,
resource_number: i64,
effective_authentication_identity: Option<&str>,
) -> rusqlite::Result<usize> {
ensure_github_read_cache_schema(conn)?;
let normalized_repository = normalize_repository(resolved_repository);
match effective_authentication_identity {
Some(identity) => conn.execute(
"DELETE FROM github_read_cache
WHERE resource_kind = ?1
AND repository = ?2
AND resource_number = ?3
AND authentication_identity_hash = ?4",
params![
resource_kind.as_str(),
normalized_repository,
resource_number,
authentication_identity_hash(identity).as_slice(),
],
),
None => conn.execute(
"DELETE FROM github_read_cache
WHERE resource_kind = ?1 AND repository = ?2 AND resource_number = ?3",
params![
resource_kind.as_str(),
normalized_repository,
resource_number
],
),
}
}
fn normalize_repository(repository: &str) -> String {
repository.trim().to_ascii_lowercase()
}
fn authentication_identity_hash(identity: &str) -> [u8; 32] {
*blake3::hash(identity.as_bytes()).as_bytes()
}