Skip to main content

imessage_database/tables/
diagnostic.rs

1/*!
2 Diagnostic result types for Messages database tables.
3*/
4
5use 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/// Diagnostic data for the `handle` table.
58#[derive(Debug)]
59pub struct HandleDiagnostic {
60    /// Total handles in the table.
61    pub total_handles: usize,
62    /// Distinct `person_centric_id` values, or `None` when the column is unavailable.
63    pub handles_with_multiple_ids: Option<usize>,
64    /// Handles deduplicated into canonical handles.
65    pub total_duplicated: usize,
66}
67
68/// Diagnostic data for the `message` table.
69#[derive(Debug)]
70pub struct MessageDiagnostic {
71    /// Total messages in the table.
72    pub total_messages: usize,
73    /// Messages not associated with any chat.
74    pub messages_without_chat: usize,
75    /// Messages that belong to more than one chat.
76    pub messages_in_multiple_chats: usize,
77    /// Recently deleted messages that are still recoverable.
78    pub recoverable_messages: Option<usize>,
79    /// Raw `date` value of the earliest message.
80    pub first_message_date: Option<i64>,
81    /// Raw `date` value of the most recent message.
82    pub last_message_date: Option<i64>,
83}
84
85/// Diagnostic data for the `attachment` table.
86#[derive(Debug)]
87pub struct AttachmentDiagnostic {
88    /// Total attachments in the table.
89    pub total_attachments: usize,
90    /// Sum of `total_bytes` for all attachment rows.
91    pub total_bytes_referenced: u64,
92    /// Total size of attachment files present on disk.
93    pub total_bytes_on_disk: u64,
94    /// Attachments with no path or no file at the resolved path.
95    pub missing_files: usize,
96    /// Attachments with no path in the table.
97    pub no_path_provided: usize,
98}
99
100impl AttachmentDiagnostic {
101    /// Attachments with a path but no file at that location.
102    #[must_use]
103    pub fn no_file_located(&self) -> usize {
104        self.missing_files.saturating_sub(self.no_path_provided)
105    }
106
107    /// Percentage of attachments that are missing.
108    #[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/// Diagnostic data for chat-handle relationships.
119#[derive(Debug)]
120pub struct ChatHandleDiagnostic {
121    /// Total chats in the table.
122    pub total_chats: usize,
123    /// Chats deduplicated into canonical chats.
124    pub total_duplicated: usize,
125    /// Chats with messages but no associated handles.
126    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}