1use crate::{Db, DbError, ReadWrite, Result};
4
5impl Db<ReadWrite> {
6 pub fn vacuum(&self) -> Result<()> {
14 self.conn.execute_batch("VACUUM").map_err(map_vacuum_err)?;
15 self.conn
16 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")
17 .map_err(map_vacuum_err)?;
18 Ok(())
19 }
20}
21
22fn map_vacuum_err(err: rusqlite::Error) -> DbError {
27 if let rusqlite::Error::SqliteFailure(e, _) = &err
28 && matches!(
29 e.code,
30 rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
31 )
32 {
33 return DbError::StoreInUse(err);
34 }
35 DbError::Sqlite(err)
36}
37
38#[cfg(test)]
39mod tests {
40 use super::map_vacuum_err;
41 use crate::models::NewArt;
42 use crate::{Db, DbError};
43
44 #[test]
45 fn vacuum_shrinks_file_and_truncates_wal_after_deletion() {
46 let dir = tempfile::tempdir().unwrap();
47 let path = dir.path().join("t.db");
48 let db = Db::open(&path).unwrap();
49
50 for i in 0..16u8 {
52 db.upsert_art(&NewArt {
53 mime: "image/png".into(),
54 width: None,
55 height: None,
56 data: vec![i; 256 * 1024],
57 })
58 .unwrap();
59 }
60 assert_eq!(db.gc_orphan_art().unwrap(), 16);
62
63 db.conn
65 .execute_batch("PRAGMA wal_checkpoint(TRUNCATE)")
66 .unwrap();
67 let before = std::fs::metadata(&path).unwrap().len();
68
69 db.vacuum().unwrap();
70
71 let after = std::fs::metadata(&path).unwrap().len();
72 assert!(after < before, "expected shrink: {before} -> {after}");
73
74 let freelist: i64 = db
75 .conn
76 .query_row("PRAGMA freelist_count", [], |r| r.get(0))
77 .unwrap();
78 assert_eq!(freelist, 0, "vacuum must leave no free pages");
79
80 let wal_frames: i64 = db
87 .conn
88 .query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |r| r.get(1))
89 .unwrap();
90 assert_eq!(wal_frames, 0, "vacuum must checkpoint the WAL");
91 }
92
93 #[test]
94 fn vacuum_on_empty_store_is_ok() {
95 let dir = tempfile::tempdir().unwrap();
96 let db = Db::open(dir.path().join("t.db")).unwrap();
97 db.vacuum().unwrap();
98 }
99
100 #[test]
101 fn map_vacuum_err_maps_busy_and_locked_to_store_in_use() {
102 use rusqlite::{Error, ffi};
103 let busy = Error::SqliteFailure(ffi::Error::new(ffi::SQLITE_BUSY), None);
104 assert!(matches!(map_vacuum_err(busy), DbError::StoreInUse(_)));
105 let locked = Error::SqliteFailure(ffi::Error::new(ffi::SQLITE_LOCKED), None);
106 assert!(matches!(map_vacuum_err(locked), DbError::StoreInUse(_)));
107 }
108
109 #[test]
110 fn map_vacuum_err_passes_through_other_errors() {
111 use rusqlite::{Error, ffi};
112 let corrupt = Error::SqliteFailure(ffi::Error::new(ffi::SQLITE_CORRUPT), None);
113 assert!(matches!(map_vacuum_err(corrupt), DbError::Sqlite(_)));
114 }
115}