Skip to main content

koan_core/db/queries/
scan_cache.rs

1use rusqlite::{Connection, params};
2
3use crate::db::connection::DbError;
4
5/// Update the scan cache entry for a file.
6pub fn update_scan_cache(
7    conn: &Connection,
8    path: &str,
9    mtime: i64,
10    size: i64,
11    track_id: i64,
12) -> Result<(), DbError> {
13    conn.execute(
14        "INSERT OR REPLACE INTO scan_cache (path, mtime, size, track_id) VALUES (?1, ?2, ?3, ?4)",
15        params![path, mtime, size, track_id],
16    )?;
17    Ok(())
18}
19
20/// Load the entire scan cache into a HashMap for batch lookups.
21/// Returns path → (mtime, size) for all cached entries.
22pub fn load_scan_cache(
23    conn: &Connection,
24) -> Result<std::collections::HashMap<String, (i64, i64)>, DbError> {
25    let mut stmt = conn.prepare("SELECT path, mtime, size FROM scan_cache")?;
26    let rows = stmt.query_map([], |row| {
27        Ok((
28            row.get::<_, String>(0)?,
29            (row.get::<_, i64>(1)?, row.get::<_, i64>(2)?),
30        ))
31    })?;
32
33    let mut map = std::collections::HashMap::new();
34    for row in rows {
35        let (path, data) = row?;
36        map.insert(path, data);
37    }
38    Ok(map)
39}
40
41/// Check if a file needs re-scanning (mtime or size changed).
42pub fn needs_rescan(conn: &Connection, path: &str, mtime: i64, size: i64) -> Result<bool, DbError> {
43    let cached = conn.query_row(
44        "SELECT mtime, size FROM scan_cache WHERE path = ?1",
45        params![path],
46        |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
47    );
48
49    match cached {
50        Ok((cached_mtime, cached_size)) => Ok(mtime != cached_mtime || size != cached_size),
51        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(true),
52        Err(e) => Err(e.into()),
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::db::connection::Database;
60    use crate::db::queries::{sample_meta, upsert_track};
61
62    fn test_db() -> Database {
63        let conn = rusqlite::Connection::open_in_memory().unwrap();
64        conn.pragma_update(None, "foreign_keys", "on").unwrap();
65        crate::db::schema::create_tables(&conn).unwrap();
66        Database { conn }
67    }
68
69    #[test]
70    fn test_scan_cache() {
71        let db = test_db();
72        let meta = sample_meta("T", "A", "Al");
73        let id = upsert_track(&db.conn, &meta).unwrap();
74
75        update_scan_cache(&db.conn, "/music/Al/T.flac", 1700000000, 30000000, id).unwrap();
76
77        // Same mtime+size → no rescan needed.
78        assert!(!needs_rescan(&db.conn, "/music/Al/T.flac", 1700000000, 30000000).unwrap());
79
80        // Different mtime → rescan.
81        assert!(needs_rescan(&db.conn, "/music/Al/T.flac", 1700000001, 30000000).unwrap());
82
83        // Unknown file → rescan.
84        assert!(needs_rescan(&db.conn, "/music/Al/New.flac", 1700000000, 30000000).unwrap());
85    }
86
87    #[test]
88    fn test_load_scan_cache() {
89        let db = test_db();
90        let meta = sample_meta("T1", "A", "Al");
91        let id1 = upsert_track(&db.conn, &meta).unwrap();
92        let meta2 = sample_meta("T2", "A", "Al");
93        let id2 = upsert_track(&db.conn, &meta2).unwrap();
94
95        update_scan_cache(&db.conn, "/music/Al/T1.flac", 100, 200, id1).unwrap();
96        update_scan_cache(&db.conn, "/music/Al/T2.flac", 300, 400, id2).unwrap();
97
98        let cache = load_scan_cache(&db.conn).unwrap();
99        assert_eq!(cache.len(), 2);
100        assert_eq!(cache.get("/music/Al/T1.flac"), Some(&(100, 200)));
101        assert_eq!(cache.get("/music/Al/T2.flac"), Some(&(300, 400)));
102        assert_eq!(cache.get("/music/Al/T3.flac"), None);
103    }
104}