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 handles_with_multiple_ids: Option<usize>,
87 pub total_duplicated: usize,
89}
90
91#[derive(Debug)]
93pub struct MessageDiagnostic {
94 pub total_messages: usize,
96 pub messages_without_chat: usize,
98 pub messages_in_multiple_chats: usize,
100 pub recoverable_messages: Option<usize>,
102 pub first_message_date: Option<i64>,
104 pub last_message_date: Option<i64>,
106}
107
108#[derive(Debug)]
110pub struct AttachmentDiagnostic {
111 pub total_attachments: usize,
113 pub total_bytes_referenced: u64,
115 pub total_bytes_on_disk: u64,
117 pub missing_files: usize,
119 pub no_path_provided: usize,
121}
122
123impl AttachmentDiagnostic {
124 #[must_use]
126 pub fn no_file_located(&self) -> usize {
127 self.missing_files.saturating_sub(self.no_path_provided)
128 }
129
130 #[must_use]
132 pub fn missing_percent(&self) -> Option<f64> {
133 if self.total_attachments > 0 {
134 Some(self.missing_files as f64 / self.total_attachments as f64 * 100.0)
135 } else {
136 None
137 }
138 }
139}
140
141#[derive(Debug)]
143pub struct ChatHandleDiagnostic {
144 pub total_chats: usize,
146 pub total_duplicated: usize,
148 pub chats_with_no_handles: usize,
150}
151
152#[cfg(test)]
153mod tests {
154 use rusqlite::Connection;
155
156 use super::{column_exists, table_exists};
157
158 #[test]
159 fn table_exists_detects_existing_and_missing_tables() {
160 let db = Connection::open_in_memory().unwrap();
161 db.execute("CREATE TABLE test_table (id INTEGER)", [])
162 .unwrap();
163
164 assert!(table_exists(&db, "test_table").unwrap());
165 assert!(!table_exists(&db, "missing_table").unwrap());
166 }
167
168 #[test]
169 fn column_exists_detects_existing_and_missing_columns() {
170 let db = Connection::open_in_memory().unwrap();
171 db.execute("CREATE TABLE test_table (id INTEGER, name TEXT)", [])
172 .unwrap();
173
174 assert!(column_exists(&db, "test_table", "name").unwrap());
175 assert!(!column_exists(&db, "test_table", "missing_column").unwrap());
176 assert!(!column_exists(&db, "missing_table", "name").unwrap());
177 }
178
179 #[test]
180 fn column_exists_quotes_table_identifiers() {
181 let db = Connection::open_in_memory().unwrap();
182 db.execute(
183 "CREATE TABLE \"quoted\"\"table\" (\"weird column\" TEXT)",
184 [],
185 )
186 .unwrap();
187
188 assert!(column_exists(&db, "quoted\"table", "weird column").unwrap());
189 }
190}