use rusqlite::Connection;
use std::path::Path;
pub fn open_wal(path: &Path) -> rusqlite::Result<Connection> {
let conn = Connection::open(path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
ensure_file_hashes_columns(&conn);
Ok(conn)
}
pub fn ensure_file_hashes_columns(conn: &Connection) {
let _ = conn.execute_batch("ALTER TABLE file_hashes ADD COLUMN mime TEXT;");
}
pub fn table_exists(conn: &Connection, name: &str) -> rusqlite::Result<bool> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
[name],
|r| r.get(0),
)?;
Ok(count > 0)
}
pub fn paths_with_known_mime(
conn: &Connection,
) -> rusqlite::Result<std::collections::HashSet<String>> {
if !table_exists(conn, "file_hashes")? {
return Ok(std::collections::HashSet::new());
}
let mut stmt = conn.prepare("SELECT path FROM file_hashes WHERE mime IS NOT NULL")?;
let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
rows.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn table_exists_true_for_existing_table() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("CREATE TABLE widgets (id INTEGER);").unwrap();
assert!(table_exists(&conn, "widgets").unwrap());
}
#[test]
fn table_exists_false_for_missing_table() {
let conn = Connection::open_in_memory().unwrap();
assert!(!table_exists(&conn, "widgets").unwrap());
}
#[test]
fn open_wal_sets_journal_mode() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let conn = open_wal(&db_path).unwrap();
let mode: String = conn
.query_row("PRAGMA journal_mode", [], |r| r.get(0))
.unwrap();
assert_eq!(mode.to_lowercase(), "wal");
}
#[test]
fn open_wal_is_idempotent_across_repeated_opens() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
open_wal(&db_path).unwrap();
let conn = open_wal(&db_path).unwrap();
let mode: String = conn
.query_row("PRAGMA journal_mode", [], |r| r.get(0))
.unwrap();
assert_eq!(mode.to_lowercase(), "wal");
}
fn db_with_rows(rows: &[(&str, Option<&str>)]) -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE file_hashes (path TEXT PRIMARY KEY, hash TEXT, ext TEXT, mime TEXT);",
)
.unwrap();
for (path, mime) in rows {
conn.execute(
"INSERT INTO file_hashes (path, hash, ext, mime) VALUES (?1, 'h', 'jpg', ?2)",
rusqlite::params![path, mime],
)
.unwrap();
}
conn
}
#[test]
fn paths_with_known_mime_excludes_null_rows() {
let conn = db_with_rows(&[("/done.jpg", Some("image/jpeg")), ("/todo.jpg", None)]);
let set = paths_with_known_mime(&conn).unwrap();
assert!(set.contains("/done.jpg"));
assert!(
!set.contains("/todo.jpg"),
"NULL means never scanned, so it must be retried"
);
}
#[test]
fn paths_with_known_mime_includes_the_sentinel() {
let conn = db_with_rows(&[("/weird.jpg", Some(crate::mime_probe::UNKNOWN_MIME))]);
assert!(paths_with_known_mime(&conn).unwrap().contains("/weird.jpg"));
}
#[test]
fn paths_with_known_mime_on_a_table_that_does_not_exist_is_empty_not_an_error() {
let conn = Connection::open_in_memory().unwrap();
assert!(paths_with_known_mime(&conn).unwrap().is_empty());
}
}