imessage_database/tables/
diagnostic.rs1use rusqlite::Connection;
6
7use crate::error::table::TableError;
8
9use std::collections::HashSet;
10
11pub(crate) fn count_query(db: &Connection, sql: &str) -> Result<usize, TableError> {
12 let count = db.prepare(sql)?.query_row([], |row| row.get::<_, i64>(0))?;
13
14 usize::try_from(count)
15 .map_err(|_| TableError::QueryError(rusqlite::Error::IntegralValueOutOfRange(0, count)))
16}
17
18pub(crate) fn table_exists(db: &Connection, table_name: &str) -> Result<bool, TableError> {
19 let exists = db.query_row(
20 "
21 SELECT EXISTS(
22 SELECT 1
23 FROM sqlite_master
24 WHERE type = 'table'
25 AND name = ?1
26 )
27 ",
28 [table_name],
29 |row| row.get::<_, i64>(0),
30 )?;
31
32 Ok(exists != 0)
33}
34
35pub(crate) fn column_exists(
36 db: &Connection,
37 table_name: &str,
38 column_name: &str,
39) -> Result<bool, TableError> {
40 let mut statement = db.prepare(&format!(
41 "PRAGMA table_info({})",
42 quote_sqlite_identifier(table_name)
43 ))?;
44 let columns = statement.query_map([], |row| row.get::<_, String>(1))?;
45
46 for column in columns {
47 if column? == column_name {
48 return Ok(true);
49 }
50 }
51
52 Ok(false)
53}
54
55pub(crate) fn column_names(
61 db: &Connection,
62 table_name: &str,
63) -> Result<HashSet<String>, TableError> {
64 let mut statement = db.prepare(&format!(
65 "PRAGMA table_info({})",
66 quote_sqlite_identifier(table_name)
67 ))?;
68 let columns = statement.query_map([], |row| row.get::<_, String>(1))?;
69 let mut names = HashSet::new();
70 for name in columns {
71 names.insert(name?.to_ascii_lowercase());
72 }
73 Ok(names)
74}
75
76fn quote_sqlite_identifier(identifier: &str) -> String {
77 format!("\"{}\"", identifier.replace('"', "\"\""))
78}
79
80#[derive(Debug)]
82pub struct HandleDiagnostic {
83 pub total_handles: usize,
85 pub person_centric_ids: Option<usize>,
87 pub multi_handle_groups: usize,
89 pub merged_handles: usize,
91}
92
93#[derive(Debug)]
95pub struct MessageDiagnostic {
96 pub total_messages: usize,
98 pub messages_without_chat: usize,
100 pub messages_in_multiple_chats: usize,
102 pub recoverable_messages: Option<usize>,
104 pub first_message_date: Option<i64>,
106 pub last_message_date: Option<i64>,
108}
109
110#[derive(Debug)]
112pub struct AttachmentDiagnostic {
113 pub total_attachments: usize,
115 pub total_bytes_referenced: u64,
117 pub total_bytes_on_disk: u64,
119 pub missing_files: usize,
121 pub no_path_provided: usize,
123}
124
125impl AttachmentDiagnostic {
126 #[must_use]
128 pub fn no_file_located(&self) -> usize {
129 self.missing_files.saturating_sub(self.no_path_provided)
130 }
131
132 #[must_use]
134 pub fn missing_percent(&self) -> Option<f64> {
135 if self.total_attachments > 0 {
136 Some(self.missing_files as f64 / self.total_attachments as f64 * 100.0)
137 } else {
138 None
139 }
140 }
141}
142
143#[derive(Debug)]
145pub struct ChatHandleDiagnostic {
146 pub total_chats: usize,
148 pub total_duplicated: usize,
150 pub chats_with_no_handles: usize,
152}
153
154#[cfg(test)]
155mod tests {
156 use rusqlite::Connection;
157
158 use super::{column_exists, table_exists};
159
160 #[test]
161 fn table_exists_detects_existing_and_missing_tables() {
162 let db = Connection::open_in_memory().unwrap();
163 db.execute("CREATE TABLE test_table (id INTEGER)", [])
164 .unwrap();
165
166 assert!(table_exists(&db, "test_table").unwrap());
167 assert!(!table_exists(&db, "missing_table").unwrap());
168 }
169
170 #[test]
171 fn column_exists_detects_existing_and_missing_columns() {
172 let db = Connection::open_in_memory().unwrap();
173 db.execute("CREATE TABLE test_table (id INTEGER, name TEXT)", [])
174 .unwrap();
175
176 assert!(column_exists(&db, "test_table", "name").unwrap());
177 assert!(!column_exists(&db, "test_table", "missing_column").unwrap());
178 assert!(!column_exists(&db, "missing_table", "name").unwrap());
179 }
180
181 #[test]
182 fn column_exists_quotes_table_identifiers() {
183 let db = Connection::open_in_memory().unwrap();
184 db.execute(
185 "CREATE TABLE \"quoted\"\"table\" (\"weird column\" TEXT)",
186 [],
187 )
188 .unwrap();
189
190 assert!(column_exists(&db, "quoted\"table", "weird column").unwrap());
191 }
192}