imessage_database/tables/
diagnostic.rs1use rusqlite::Connection;
6
7use crate::error::table::TableError;
8
9pub(crate) fn count_query(db: &Connection, sql: &str) -> Result<usize, TableError> {
10 let count = db.prepare(sql)?.query_row([], |row| row.get::<_, i64>(0))?;
11
12 usize::try_from(count)
13 .map_err(|_| TableError::QueryError(rusqlite::Error::IntegralValueOutOfRange(0, count)))
14}
15
16pub(crate) fn table_exists(db: &Connection, table_name: &str) -> Result<bool, TableError> {
17 let exists = db.query_row(
18 "
19 SELECT EXISTS(
20 SELECT 1
21 FROM sqlite_master
22 WHERE type = 'table'
23 AND name = ?1
24 )
25 ",
26 [table_name],
27 |row| row.get::<_, i64>(0),
28 )?;
29
30 Ok(exists != 0)
31}
32
33pub(crate) fn column_exists(
34 db: &Connection,
35 table_name: &str,
36 column_name: &str,
37) -> Result<bool, TableError> {
38 let mut statement = db.prepare(&format!(
39 "PRAGMA table_info({})",
40 quote_sqlite_identifier(table_name)
41 ))?;
42 let columns = statement.query_map([], |row| row.get::<_, String>(1))?;
43
44 for column in columns {
45 if column? == column_name {
46 return Ok(true);
47 }
48 }
49
50 Ok(false)
51}
52
53fn quote_sqlite_identifier(identifier: &str) -> String {
54 format!("\"{}\"", identifier.replace('"', "\"\""))
55}
56
57#[derive(Debug)]
59pub struct HandleDiagnostic {
60 pub total_handles: usize,
62 pub handles_with_multiple_ids: Option<usize>,
64 pub total_duplicated: usize,
66}
67
68#[derive(Debug)]
70pub struct MessageDiagnostic {
71 pub total_messages: usize,
73 pub messages_without_chat: usize,
75 pub messages_in_multiple_chats: usize,
77 pub recoverable_messages: Option<usize>,
79 pub first_message_date: Option<i64>,
81 pub last_message_date: Option<i64>,
83}
84
85#[derive(Debug)]
87pub struct AttachmentDiagnostic {
88 pub total_attachments: usize,
90 pub total_bytes_referenced: u64,
92 pub total_bytes_on_disk: u64,
94 pub missing_files: usize,
96 pub no_path_provided: usize,
98}
99
100impl AttachmentDiagnostic {
101 #[must_use]
103 pub fn no_file_located(&self) -> usize {
104 self.missing_files.saturating_sub(self.no_path_provided)
105 }
106
107 #[must_use]
109 pub fn missing_percent(&self) -> Option<f64> {
110 if self.total_attachments > 0 {
111 Some(self.missing_files as f64 / self.total_attachments as f64 * 100.0)
112 } else {
113 None
114 }
115 }
116}
117
118#[derive(Debug)]
120pub struct ChatHandleDiagnostic {
121 pub total_chats: usize,
123 pub total_duplicated: usize,
125 pub chats_with_no_handles: usize,
127}
128
129#[cfg(test)]
130mod tests {
131 use rusqlite::Connection;
132
133 use super::{column_exists, table_exists};
134
135 #[test]
136 fn table_exists_detects_existing_and_missing_tables() {
137 let db = Connection::open_in_memory().unwrap();
138 db.execute("CREATE TABLE test_table (id INTEGER)", [])
139 .unwrap();
140
141 assert!(table_exists(&db, "test_table").unwrap());
142 assert!(!table_exists(&db, "missing_table").unwrap());
143 }
144
145 #[test]
146 fn column_exists_detects_existing_and_missing_columns() {
147 let db = Connection::open_in_memory().unwrap();
148 db.execute("CREATE TABLE test_table (id INTEGER, name TEXT)", [])
149 .unwrap();
150
151 assert!(column_exists(&db, "test_table", "name").unwrap());
152 assert!(!column_exists(&db, "test_table", "missing_column").unwrap());
153 assert!(!column_exists(&db, "missing_table", "name").unwrap());
154 }
155
156 #[test]
157 fn column_exists_quotes_table_identifiers() {
158 let db = Connection::open_in_memory().unwrap();
159 db.execute(
160 "CREATE TABLE \"quoted\"\"table\" (\"weird column\" TEXT)",
161 [],
162 )
163 .unwrap();
164
165 assert!(column_exists(&db, "quoted\"table", "weird column").unwrap());
166 }
167}