smriti/db/
excluded_folder_repo.rs1use rusqlite::{params, Connection, Result as SqliteResult};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ExcludedFolderRecord {
7 pub relative_path: String,
8 pub created_at: String,
9 pub indexed_count: i64,
10}
11
12pub struct ExcludedFolderRepo<'a> {
13 conn: &'a Connection,
14}
15
16impl<'a> ExcludedFolderRepo<'a> {
17 pub fn new(conn: &'a Connection) -> Self {
18 Self { conn }
19 }
20
21 pub fn list(&self) -> SqliteResult<Vec<ExcludedFolderRecord>> {
22 let mut stmt = self.conn.prepare(
23 "SELECT e.relative_path, e.created_at,
24 (SELECT COUNT(*) FROM photos p
25 WHERE p.is_trashed = FALSE
26 AND (p.file_path = e.relative_path OR p.file_path LIKE e.relative_path || '/%'))
27 FROM excluded_folders e
28 ORDER BY e.relative_path COLLATE NOCASE",
29 )?;
30 let records = stmt
31 .query_map([], |row| {
32 Ok(ExcludedFolderRecord {
33 relative_path: row.get(0)?,
34 created_at: row.get(1)?,
35 indexed_count: row.get(2)?,
36 })
37 })?
38 .collect();
39 records
40 }
41
42 pub fn relative_paths(&self) -> SqliteResult<Vec<String>> {
43 let mut stmt = self
44 .conn
45 .prepare("SELECT relative_path FROM excluded_folders ORDER BY relative_path")?;
46 let paths = stmt.query_map([], |row| row.get(0))?.collect();
47 paths
48 }
49
50 pub fn count_indexed_under(&self, relative_path: &str) -> SqliteResult<i64> {
51 self.conn.query_row(
52 "SELECT COUNT(*) FROM photos
53 WHERE is_trashed = FALSE
54 AND (file_path = ?1 OR file_path LIKE ?1 || '/%')",
55 params![relative_path],
56 |row| row.get(0),
57 )
58 }
59
60 pub fn insert_and_remove_indexed(
61 &self,
62 relative_path: &str,
63 ) -> SqliteResult<ExcludedFolderRecord> {
64 let tx = self.conn.unchecked_transaction()?;
65 tx.execute(
66 "INSERT OR IGNORE INTO excluded_folders (relative_path) VALUES (?1)",
67 params![relative_path],
68 )?;
69 tx.execute(
70 "DELETE FROM photos
71 WHERE file_path = ?1 OR file_path LIKE ?1 || '/%'",
72 params![relative_path],
73 )?;
74 tx.commit()?;
75 self.get(relative_path)
76 }
77
78 pub fn remove(&self, relative_path: &str) -> SqliteResult<bool> {
79 let removed = self.conn.execute(
80 "DELETE FROM excluded_folders WHERE relative_path = ?1",
81 params![relative_path],
82 )?;
83 Ok(removed > 0)
84 }
85
86 fn get(&self, relative_path: &str) -> SqliteResult<ExcludedFolderRecord> {
87 self.conn.query_row(
88 "SELECT e.relative_path, e.created_at,
89 (SELECT COUNT(*) FROM photos p
90 WHERE p.is_trashed = FALSE
91 AND (p.file_path = e.relative_path OR p.file_path LIKE e.relative_path || '/%'))
92 FROM excluded_folders e
93 WHERE e.relative_path = ?1",
94 params![relative_path],
95 |row| {
96 Ok(ExcludedFolderRecord {
97 relative_path: row.get(0)?,
98 created_at: row.get(1)?,
99 indexed_count: row.get(2)?,
100 })
101 },
102 )
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use rusqlite::Connection;
110
111 #[test]
112 fn counts_and_removes_only_folder_descendants() {
113 let conn = Connection::open_in_memory().unwrap();
114 crate::db::create_schema(&conn).unwrap();
115 conn.execute(
116 "INSERT INTO photos (file_path, file_name, file_hash, file_size)
117 VALUES
118 ('Trips/Goa/a.jpg', 'a.jpg', 'h1', 12000),
119 ('Trips/Goa/Sub/b.jpg', 'b.jpg', 'h2', 12000),
120 ('Trips/Goa2/c.jpg', 'c.jpg', 'h3', 12000),
121 ('Other/Goa/d.jpg', 'd.jpg', 'h4', 12000)",
122 [],
123 )
124 .unwrap();
125
126 let repo = ExcludedFolderRepo::new(&conn);
127 assert_eq!(repo.count_indexed_under("Trips/Goa").unwrap(), 2);
128
129 let record = repo.insert_and_remove_indexed("Trips/Goa").unwrap();
130 assert_eq!(record.relative_path, "Trips/Goa");
131 assert_eq!(record.indexed_count, 0);
132
133 let remaining: i64 = conn
134 .query_row("SELECT COUNT(*) FROM photos", [], |row| row.get(0))
135 .unwrap();
136 assert_eq!(remaining, 2);
137 }
138}