data/MailCacheSchema.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "data/MailCacheSchema.h" | ||
| 2 | |||
| 3 | #include "data/MailCacheSql.h" | ||
| 4 | #include "data/ThreadProjectionSchema.h" | ||
| 5 | #include <QLoggingCategory> | ||
| 6 | #include <QSqlError> | ||
| 7 | #include <QSqlQuery> | ||
| 8 | #include <QVariant> | ||
| 9 | |||
| 10 | // Same category as MailCache (defined there), see docs/LOGGING.md. | ||
| 11 | Q_DECLARE_LOGGING_CATEGORY(lcCache) | ||
| 12 | |||
| 13 | using MailCacheSql::AppendPayloadSchema; | ||
| 14 | |||
| 15 | 5035 | MailCacheSchema::MailCacheSchema(QSqlDatabase &db, QString &lastError) | |
| 16 | 5035 | : m_db(db), m_lastError(lastError) {} | |
| 17 | |||
| 18 | // Install and backfill derived counts atomically. Reopening a complete cache | ||
| 19 | // only inspects schema metadata; it never recounts all headers. | ||
| 20 | 1691 | static bool ensureHeaderCounts(QSqlDatabase &db, QString *error) { | |
| 21 |
1/1✓ Branch 2 → 3 taken 1691 times.
|
1691 | QSqlQuery query(db); |
| 22 | 3249 | const auto complete = [&]() -> std::optional<bool> { | |
| 23 |
3/8✓ Branch 13 → 14 taken 3249 times.
✗ Branch 13 → 15 not taken.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 31 taken 3247 times.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
|
6498 | if (!query.exec(QStringLiteral( |
| 24 | "SELECT count(*) FROM sqlite_master WHERE " | ||
| 25 | "(type='table' AND name='header_counts') OR " | ||
| 26 | "(type='trigger' AND name IN " | ||
| 27 |
4/5✓ Branch 5 → 6 taken 3249 times.
✓ Branch 6 → 7 taken 3247 times.
✓ Branch 6 → 9 taken 2 times.
✓ Branch 11 → 12 taken 3249 times.
✗ Branch 11 → 13 not taken.
|
9745 | "('header_counts_ai','header_counts_ad','header_counts_au'))")) || |
| 28 |
2/3✓ Branch 7 → 8 taken 3247 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3247 times.
|
3247 | !query.next()) { |
| 29 |
3/3✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
|
4 | *error = QStringLiteral("Cannot inspect header count schema: ") + query.lastError().text(); |
| 30 | 2 | return std::nullopt; | |
| 31 | } | ||
| 32 |
2/2✓ Branch 31 → 32 taken 3247 times.
✓ Branch 32 → 33 taken 3247 times.
|
3247 | const bool ready = query.value(0).toInt() == 4; |
| 33 |
1/1✓ Branch 34 → 35 taken 3247 times.
|
3247 | query.finish(); |
| 34 | 3247 | return ready; | |
| 35 | 1691 | }; | |
| 36 |
1/1✓ Branch 3 → 4 taken 1691 times.
|
1691 | auto ready = complete(); |
| 37 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 1690 times.
|
1691 | if (!ready) return false; |
| 38 |
2/2✓ Branch 8 → 9 taken 131 times.
✓ Branch 8 → 10 taken 1559 times.
|
1690 | if (*ready) return true; |
| 39 |
3/3✓ Branch 13 → 14 taken 1559 times.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 23 taken 1558 times.
|
1559 | if (!query.exec(QStringLiteral("BEGIN IMMEDIATE"))) { |
| 40 |
2/2✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
1 | *error = query.lastError().text(); |
| 41 | 1 | return false; | |
| 42 | } | ||
| 43 | 1567 | auto rollback = qScopeGuard([&] { db.rollback(); }); | |
| 44 | // Another connection may have completed installation while we waited. | ||
| 45 |
1/1✓ Branch 24 → 25 taken 1558 times.
|
1558 | ready = complete(); |
| 46 |
2/2✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 28 taken 1557 times.
|
1558 | if (!ready) return false; |
| 47 |
1/2✓ Branch 29 → 30 taken 1557 times.
✗ Branch 29 → 85 not taken.
|
1557 | if (!*ready) { |
| 48 | const QStringList statements = { | ||
| 49 | ✗ | QStringLiteral( | |
| 50 | "CREATE TABLE IF NOT EXISTS header_counts (" | ||
| 51 | "folder_id INTEGER PRIMARY KEY REFERENCES folders(id) ON DELETE CASCADE," | ||
| 52 | "total INTEGER NOT NULL CHECK(total>=0)," | ||
| 53 | "unread INTEGER NOT NULL CHECK(unread>=0 AND unread<=total))"), | ||
| 54 | 1557 | QStringLiteral("DELETE FROM header_counts"), | |
| 55 | 1557 | QStringLiteral( | |
| 56 | "INSERT INTO header_counts SELECT folder_id,count(*)," | ||
| 57 | "sum(coalesce((flags & 1)=0,0)) FROM headers GROUP BY folder_id"), | ||
| 58 | 1557 | QStringLiteral( | |
| 59 | "CREATE TRIGGER IF NOT EXISTS header_counts_ai AFTER INSERT ON headers BEGIN " | ||
| 60 | "INSERT INTO header_counts VALUES(new.folder_id,1,coalesce((new.flags & 1)=0,0)) " | ||
| 61 | "ON CONFLICT(folder_id) DO UPDATE SET total=total+1,unread=unread+excluded.unread; END"), | ||
| 62 | 1557 | QStringLiteral( | |
| 63 | "CREATE TRIGGER IF NOT EXISTS header_counts_ad AFTER DELETE ON headers BEGIN " | ||
| 64 | "UPDATE header_counts SET total=total-1,unread=unread-coalesce((old.flags & 1)=0,0) " | ||
| 65 | "WHERE folder_id=old.folder_id; END"), | ||
| 66 | 1557 | QStringLiteral( | |
| 67 | "CREATE TRIGGER IF NOT EXISTS header_counts_au AFTER UPDATE OF folder_id,flags ON headers " | ||
| 68 | "WHEN old.folder_id IS NOT new.folder_id OR (old.flags & 1) IS NOT (new.flags & 1) BEGIN " | ||
| 69 | "UPDATE header_counts SET total=total-1,unread=unread-coalesce((old.flags & 1)=0,0) " | ||
| 70 | "WHERE folder_id=old.folder_id; " | ||
| 71 | "INSERT INTO header_counts VALUES(new.folder_id,1,coalesce((new.flags & 1)=0,0)) " | ||
| 72 | "ON CONFLICT(folder_id) DO UPDATE SET total=total+1,unread=unread+excluded.unread; END"), | ||
| 73 |
2/4✓ Branch 50 → 51 taken 9342 times.
✓ Branch 50 → 52 taken 1557 times.
✗ Branch 110 → 111 not taken.
✗ Branch 110 → 112 not taken.
|
10899 | }; |
| 74 |
2/2✓ Branch 78 → 60 taken 9324 times.
✓ Branch 78 → 79 taken 1550 times.
|
10874 | for (const auto &statement : statements) { |
| 75 |
3/3✓ Branch 61 → 62 taken 9324 times.
✓ Branch 62 → 63 taken 7 times.
✓ Branch 62 → 76 taken 9317 times.
|
9324 | if (!query.exec(statement)) { |
| 76 |
3/3✓ Branch 63 → 64 taken 7 times.
✓ Branch 64 → 65 taken 7 times.
✓ Branch 68 → 69 taken 7 times.
|
14 | *error = QStringLiteral("Cannot initialize header counts: ") + query.lastError().text(); |
| 77 | 7 | return false; | |
| 78 | } | ||
| 79 | } | ||
| 80 | 1557 | } | |
| 81 |
3/3✓ Branch 85 → 86 taken 1550 times.
✓ Branch 86 → 87 taken 1 time.
✓ Branch 86 → 93 taken 1549 times.
|
1550 | if (!db.commit()) { |
| 82 |
2/2✓ Branch 87 → 88 taken 1 time.
✓ Branch 88 → 89 taken 1 time.
|
1 | *error = db.lastError().text(); |
| 83 | 1 | return false; | |
| 84 | } | ||
| 85 | 1549 | rollback.dismiss(); | |
| 86 | 1549 | return true; | |
| 87 | 12590 | } | |
| 88 | |||
| 89 | // Tokens change only when cached message identities change. A fresh token on | ||
| 90 | // repair also invalidates predictor checkpoints from an older cache generation. | ||
| 91 | 1680 | static bool ensureTrainingRevisions(QSqlDatabase &db, QString *error) { | |
| 92 |
1/1✓ Branch 2 → 3 taken 1680 times.
|
1680 | QSqlQuery query(db); |
| 93 |
2/8✓ Branch 14 → 15 taken 1680 times.
✗ Branch 14 → 16 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 23 taken 1680 times.
✗ Branch 103 → 104 not taken.
✗ Branch 103 → 105 not taken.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
|
3360 | if (!query.exec(QStringLiteral( |
| 94 | "SELECT count(*) FROM sqlite_master WHERE " | ||
| 95 | "(type='table' AND name='training_revisions') OR " | ||
| 96 | "(type='trigger' AND name IN ('training_revisions_ai'," | ||
| 97 |
5/8✓ Branch 6 → 7 taken 1680 times.
✓ Branch 7 → 8 taken 1680 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 1680 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1680 times.
✓ Branch 12 → 13 taken 1680 times.
✗ Branch 12 → 14 not taken.
|
3360 | "'training_revisions_ad','training_revisions_au'))")) || !query.next()) { |
| 98 | ✗ | *error = query.lastError().text(); | |
| 99 | ✗ | return false; | |
| 100 | } | ||
| 101 |
2/2✓ Branch 23 → 24 taken 1680 times.
✓ Branch 24 → 25 taken 1680 times.
|
1680 | const bool ready = query.value(0).toInt() == 4; |
| 102 |
1/1✓ Branch 26 → 27 taken 1680 times.
|
1680 | query.finish(); |
| 103 |
2/2✓ Branch 27 → 28 taken 129 times.
✓ Branch 27 → 29 taken 1551 times.
|
1680 | if (ready) return true; |
| 104 |
3/3✓ Branch 32 → 33 taken 1551 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 42 taken 1550 times.
|
1551 | if (!query.exec(QStringLiteral("BEGIN IMMEDIATE"))) { |
| 105 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
|
1 | *error = query.lastError().text(); return false; |
| 106 | } | ||
| 107 | 1556 | auto rollback = qScopeGuard([&] { db.rollback(); }); | |
| 108 | const QStringList statements = { | ||
| 109 | ✗ | QStringLiteral("CREATE TABLE IF NOT EXISTS training_revisions (" | |
| 110 | "folder_id INTEGER PRIMARY KEY REFERENCES folders(id) ON DELETE CASCADE," | ||
| 111 | "revision TEXT NOT NULL)"), | ||
| 112 | 1550 | QStringLiteral("INSERT INTO training_revisions SELECT id,hex(randomblob(16)) FROM folders WHERE true " | |
| 113 | "ON CONFLICT(folder_id) DO UPDATE SET revision=excluded.revision"), | ||
| 114 | 1550 | QStringLiteral("CREATE TRIGGER IF NOT EXISTS training_revisions_ai AFTER INSERT ON headers BEGIN " | |
| 115 | "INSERT INTO training_revisions VALUES(new.folder_id,hex(randomblob(16))) " | ||
| 116 | "ON CONFLICT(folder_id) DO UPDATE SET revision=excluded.revision; END"), | ||
| 117 | 1550 | QStringLiteral("CREATE TRIGGER IF NOT EXISTS training_revisions_ad AFTER DELETE ON headers BEGIN " | |
| 118 | "UPDATE training_revisions SET revision=hex(randomblob(16)) WHERE folder_id=old.folder_id; END"), | ||
| 119 | 1550 | QStringLiteral("CREATE TRIGGER IF NOT EXISTS training_revisions_au AFTER UPDATE OF folder_id,uid ON headers " | |
| 120 | "WHEN old.folder_id IS NOT new.folder_id OR old.uid IS NOT new.uid BEGIN " | ||
| 121 | "UPDATE training_revisions SET revision=hex(randomblob(16)) WHERE folder_id=old.folder_id; " | ||
| 122 | "INSERT INTO training_revisions VALUES(new.folder_id,hex(randomblob(16))) " | ||
| 123 | "ON CONFLICT(folder_id) DO UPDATE SET revision=excluded.revision; END") | ||
| 124 |
2/4✓ Branch 60 → 61 taken 7750 times.
✓ Branch 60 → 62 taken 1550 times.
✗ Branch 129 → 130 not taken.
✗ Branch 129 → 131 not taken.
|
9300 | }; |
| 125 |
2/2✓ Branch 87 → 69 taken 7740 times.
✓ Branch 87 → 88 taken 1545 times.
|
9285 | for (const auto &statement : statements) { |
| 126 |
3/3✓ Branch 70 → 71 taken 7740 times.
✓ Branch 71 → 72 taken 5 times.
✓ Branch 71 → 85 taken 7735 times.
|
7740 | if (!query.exec(statement)) { |
| 127 |
3/3✓ Branch 72 → 73 taken 5 times.
✓ Branch 73 → 74 taken 5 times.
✓ Branch 77 → 78 taken 5 times.
|
10 | *error = QStringLiteral("Cannot initialize predictor revisions: ") + query.lastError().text(); |
| 128 | 5 | return false; | |
| 129 | } | ||
| 130 | } | ||
| 131 |
5/5✓ Branch 88 → 89 taken 1545 times.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 96 taken 1544 times.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 91 → 92 taken 1 time.
|
1545 | if (!db.commit()) { *error = db.lastError().text(); return false; } |
| 132 | 1544 | rollback.dismiss(); | |
| 133 | 1544 | return true; | |
| 134 | 10980 | } | |
| 135 | |||
| 136 | // createSchema step: folders, headers and bodies with their indexes and derived counters. | ||
| 137 | 1694 | bool MailCacheSchema::createMessageTables() { | |
| 138 |
1/1✓ Branch 2 → 3 taken 1694 times.
|
1694 | QSqlQuery q(m_db); |
| 139 | |||
| 140 |
1/1✓ Branch 6 → 7 taken 1694 times.
|
1694 | bool ok = q.exec(QStringLiteral("CREATE TABLE IF NOT EXISTS folders (" |
| 141 | " id INTEGER PRIMARY KEY AUTOINCREMENT," | ||
| 142 | " account TEXT NOT NULL," | ||
| 143 | " path TEXT NOT NULL," | ||
| 144 | " uidvalidity INTEGER DEFAULT 0," | ||
| 145 | " last_sync INTEGER DEFAULT 0," | ||
| 146 | " highest_modseq INTEGER DEFAULT 0," | ||
| 147 | " UNIQUE(account, path)" | ||
| 148 | ")")); | ||
| 149 |
2/2✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 26 taken 1693 times.
|
1694 | if (!ok) { |
| 150 |
5/5✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 24 → 12 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create folders table:" |
| 151 |
3/3✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
1 | << q.lastError().text(); |
| 152 | 1 | return false; | |
| 153 | } | ||
| 154 | |||
| 155 |
1/1✓ Branch 29 → 30 taken 1693 times.
|
1693 | ok = q.exec(QStringLiteral( |
| 156 | "CREATE TABLE IF NOT EXISTS headers (" | ||
| 157 | " id INTEGER PRIMARY KEY AUTOINCREMENT," | ||
| 158 | " folder_id INTEGER NOT NULL REFERENCES folders(id) ON DELETE CASCADE," | ||
| 159 | " uid INTEGER NOT NULL," | ||
| 160 | " subject TEXT," | ||
| 161 | " from_addr TEXT," | ||
| 162 | " to_addr TEXT," | ||
| 163 | " date INTEGER," | ||
| 164 | " flags INTEGER DEFAULT 0," | ||
| 165 | " size INTEGER DEFAULT 0," | ||
| 166 | " has_attachments INTEGER DEFAULT 0," | ||
| 167 | " message_id TEXT," | ||
| 168 | " in_reply_to TEXT," | ||
| 169 | " ref_ids TEXT," | ||
| 170 | " is_spam INTEGER DEFAULT 0," | ||
| 171 | " cc_addr TEXT," // NULL = not known (S95.1.11) | ||
| 172 | " list_id TEXT," | ||
| 173 | // Sprint 105 S105.2.3: the server spam filter (NULL = no filter field). | ||
| 174 | " spam_score REAL," | ||
| 175 | " spam_threshold REAL," | ||
| 176 | " spam_tool TEXT," | ||
| 177 | " UNIQUE(folder_id, uid)" | ||
| 178 | ")")); | ||
| 179 |
2/2✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 49 taken 1692 times.
|
1693 | if (!ok) { |
| 180 |
5/5✓ Branch 33 → 34 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 47 → 35 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create headers table:" |
| 181 |
3/3✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
|
1 | << q.lastError().text(); |
| 182 | 1 | return false; | |
| 183 | } | ||
| 184 | |||
| 185 |
1/1✓ Branch 52 → 53 taken 1692 times.
|
1692 | ok = q.exec(QStringLiteral("CREATE TABLE IF NOT EXISTS bodies (" |
| 186 | " header_id INTEGER PRIMARY KEY REFERENCES " | ||
| 187 | "headers(id) ON DELETE CASCADE," | ||
| 188 | " text_plain TEXT," | ||
| 189 | " text_html TEXT," | ||
| 190 | " raw_body BLOB," | ||
| 191 | " fetched_at INTEGER" | ||
| 192 | ")")); | ||
| 193 |
2/2✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 72 taken 1691 times.
|
1692 | if (!ok) { |
| 194 |
5/5✓ Branch 56 → 57 taken 1 time.
✓ Branch 60 → 61 taken 1 time.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 70 → 58 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create bodies table:" |
| 195 |
3/3✓ Branch 62 → 63 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
|
1 | << q.lastError().text(); |
| 196 | 1 | return false; | |
| 197 | } | ||
| 198 | |||
| 199 | // Indexes for fast folder-based queries | ||
| 200 |
1/1✓ Branch 75 → 76 taken 1691 times.
|
1691 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_headers_folder_date " |
| 201 | "ON headers(folder_id, date DESC)")); | ||
| 202 |
1/1✓ Branch 81 → 82 taken 1691 times.
|
1691 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_headers_folder_page " |
| 203 | "ON headers(folder_id, date DESC, uid DESC)")); | ||
| 204 |
1/1✓ Branch 87 → 88 taken 1691 times.
|
1691 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_headers_folder_uid " |
| 205 | "ON headers(folder_id, uid)")); | ||
| 206 | // Both unread aggregates and bounded mark-read pages use this predicate. | ||
| 207 | // Avoid fetching every wide header row just to inspect its Seen bit. | ||
| 208 |
1/1✓ Branch 93 → 94 taken 1691 times.
|
1691 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_headers_unread_uid " |
| 209 | "ON headers(folder_id, uid) WHERE (flags & 1) = 0")); | ||
| 210 |
3/3✓ Branch 96 → 97 taken 1691 times.
✓ Branch 97 → 98 taken 11 times.
✓ Branch 97 → 99 taken 1680 times.
|
1691 | if (!ensureHeaderCounts(m_db, &m_lastError)) |
| 211 | 11 | return false; | |
| 212 |
3/3✓ Branch 99 → 100 taken 1680 times.
✓ Branch 100 → 101 taken 7 times.
✓ Branch 100 → 102 taken 1673 times.
|
1680 | if (!ensureTrainingRevisions(m_db, &m_lastError)) |
| 213 | 7 | return false; | |
| 214 | // T-096: Index for thread lookups by message_id | ||
| 215 |
1/1✓ Branch 105 → 106 taken 1673 times.
|
1673 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_headers_msgid " |
| 216 | "ON headers(message_id)")); | ||
| 217 |
1/1✓ Branch 111 → 112 taken 1673 times.
|
1673 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_bodies_fetched_at " |
| 218 | "ON bodies(fetched_at)")); | ||
| 219 | 1673 | return true; | |
| 220 | 1694 | } | |
| 221 | |||
| 222 | // createSchema step: attachments, folder badges and IMAP keyword labels. | ||
| 223 | 1673 | bool MailCacheSchema::createAttachmentAndLabelTables() { | |
| 224 |
1/1✓ Branch 2 → 3 taken 1673 times.
|
1673 | QSqlQuery q(m_db); |
| 225 | 1673 | bool ok = false; | |
| 226 | // Attachments table: BLOB data stored for lazy loading | ||
| 227 |
1/1✓ Branch 6 → 7 taken 1673 times.
|
1673 | ok = q.exec(QStringLiteral( |
| 228 | "CREATE TABLE IF NOT EXISTS attachments (" | ||
| 229 | " id INTEGER PRIMARY KEY AUTOINCREMENT," | ||
| 230 | " header_id INTEGER NOT NULL REFERENCES headers(id) ON DELETE CASCADE," | ||
| 231 | " filename TEXT," | ||
| 232 | " content_type TEXT," | ||
| 233 | " size INTEGER DEFAULT 0," | ||
| 234 | " content_id TEXT," | ||
| 235 | " presentation INTEGER DEFAULT 0," | ||
| 236 | " data BLOB" | ||
| 237 | ")")); | ||
| 238 |
2/2✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 26 taken 1672 times.
|
1673 | if (!ok) { |
| 239 |
5/5✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 24 → 12 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create attachments table:" |
| 240 |
3/3✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
1 | << q.lastError().text(); |
| 241 | 1 | return false; | |
| 242 | } | ||
| 243 | |||
| 244 |
1/1✓ Branch 29 → 30 taken 1672 times.
|
1672 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_attachments_header " |
| 245 | "ON attachments(header_id)")); | ||
| 246 | |||
| 247 | // T-075: Badge cache table for persisting STATUS UNSEEN counts | ||
| 248 |
1/1✓ Branch 35 → 36 taken 1672 times.
|
1672 | ok = q.exec(QStringLiteral( |
| 249 | "CREATE TABLE IF NOT EXISTS folder_badges (" | ||
| 250 | " folder_id INTEGER PRIMARY KEY REFERENCES folders(id) ON DELETE CASCADE," | ||
| 251 | " unseen INTEGER DEFAULT 0," | ||
| 252 | " updated_at INTEGER DEFAULT 0" | ||
| 253 | ")")); | ||
| 254 |
2/2✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 55 taken 1671 times.
|
1672 | if (!ok) { |
| 255 |
5/5✓ Branch 39 → 40 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 53 → 41 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create folder_badges table:" |
| 256 |
3/3✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
|
1 | << q.lastError().text(); |
| 257 | 1 | return false; | |
| 258 | } | ||
| 259 | |||
| 260 | // T-086: Labels table for IMAP keywords | ||
| 261 |
1/1✓ Branch 58 → 59 taken 1671 times.
|
1671 | ok = q.exec(QStringLiteral( |
| 262 | "CREATE TABLE IF NOT EXISTS mail_labels (" | ||
| 263 | " id INTEGER PRIMARY KEY AUTOINCREMENT," | ||
| 264 | " header_id INTEGER NOT NULL REFERENCES headers(id) ON DELETE CASCADE," | ||
| 265 | " label TEXT NOT NULL," | ||
| 266 | " UNIQUE(header_id, label)" | ||
| 267 | ")")); | ||
| 268 |
2/2✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 78 taken 1670 times.
|
1671 | if (!ok) { |
| 269 |
5/5✓ Branch 62 → 63 taken 1 time.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 76 → 64 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create mail_labels table:" |
| 270 |
3/3✓ Branch 68 → 69 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
|
1 | << q.lastError().text(); |
| 271 | 1 | return false; | |
| 272 | } | ||
| 273 | |||
| 274 |
1/1✓ Branch 81 → 82 taken 1670 times.
|
1670 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_mail_labels_header " |
| 275 | "ON mail_labels(header_id)")); | ||
| 276 | |||
| 277 | // Sprint 105 S105.5.1 (D-7, schema v26): one row per mail and address | ||
| 278 | // (MailAddressIndex: normalized, role 0 From / 1 To / 2 Cc) for the | ||
| 279 | // contact history. Rows follow their header (ON DELETE CASCADE); the | ||
| 280 | // writer replaces them with the header row. cache_meta | ||
| 281 | // 'address_index_watermark' exists only in caches migrated from v25: the | ||
| 282 | // highest header id the background backfill indexed so far (no row = | ||
| 283 | // complete; rows written since v26 are always indexed by the writer). | ||
| 284 |
1/1✓ Branch 87 → 88 taken 1670 times.
|
1670 | ok = q.exec(QStringLiteral( |
| 285 | "CREATE TABLE IF NOT EXISTS message_addresses (" | ||
| 286 | " header_id INTEGER NOT NULL REFERENCES headers(id) ON DELETE CASCADE," | ||
| 287 | " address TEXT NOT NULL," | ||
| 288 | " role INTEGER NOT NULL," | ||
| 289 | " date INTEGER," | ||
| 290 | " PRIMARY KEY(header_id, role, address)" | ||
| 291 | ") WITHOUT ROWID")); | ||
| 292 |
1/2✗ Branch 90 → 91 not taken.
✓ Branch 90 → 107 taken 1670 times.
|
1670 | if (!ok) { |
| 293 | ✗ | qCWarning(lcCache) << "Failed to create message_addresses table:" << q.lastError().text(); | |
| 294 | ✗ | return false; | |
| 295 | } | ||
| 296 |
1/1✓ Branch 110 → 111 taken 1670 times.
|
1670 | q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_message_addresses_address_date " |
| 297 | "ON message_addresses(address, date DESC)")); | ||
| 298 | 1670 | return true; | |
| 299 | 1673 | } | |
| 300 | |||
| 301 | // createSchema step: the durable mutation journal and its APPEND payloads. | ||
| 302 | 1670 | bool MailCacheSchema::createMutationJournalTables() { | |
| 303 |
1/1✓ Branch 2 → 3 taken 1670 times.
|
1670 | QSqlQuery q(m_db); |
| 304 | 1670 | bool ok = false; | |
| 305 | // Sprint 84: cache-adjacent durable intent. Do not add message content or | ||
| 306 | // credentials here. The bounded text columns contain only mailbox identity, | ||
| 307 | // flag/target, Message-ID, UIDPLUS evidence, and a terminal diagnostic. | ||
| 308 |
1/1✓ Branch 6 → 7 taken 1670 times.
|
1670 | ok = q.exec(QStringLiteral( |
| 309 | "CREATE TABLE IF NOT EXISTS mutation_journal (" | ||
| 310 | " token INTEGER PRIMARY KEY AUTOINCREMENT," | ||
| 311 | " account_id TEXT NOT NULL," | ||
| 312 | " folder_id INTEGER NOT NULL," | ||
| 313 | " folder_path TEXT NOT NULL," | ||
| 314 | " uid INTEGER NOT NULL," | ||
| 315 | " kind INTEGER NOT NULL," | ||
| 316 | " replay_class INTEGER NOT NULL," | ||
| 317 | " phase INTEGER NOT NULL," | ||
| 318 | " flag_name TEXT DEFAULT ''," | ||
| 319 | " desired_state INTEGER DEFAULT 0," | ||
| 320 | " target_folder TEXT DEFAULT ''," | ||
| 321 | " message_id TEXT DEFAULT ''," | ||
| 322 | " uidvalidity INTEGER DEFAULT 0," | ||
| 323 | " uidplus_evidence TEXT DEFAULT ''," | ||
| 324 | " error TEXT DEFAULT ''," | ||
| 325 | " created_at INTEGER NOT NULL," | ||
| 326 | " updated_at INTEGER NOT NULL" | ||
| 327 | ")")); | ||
| 328 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 26 taken 1670 times.
|
1670 | if (!ok) { |
| 329 | ✗ | qCWarning(lcCache) << "Failed to create mutation_journal table:" | |
| 330 | ✗ | << q.lastError().text(); | |
| 331 | ✗ | return false; | |
| 332 | } | ||
| 333 |
1/1✓ Branch 29 → 30 taken 1670 times.
|
1670 | q.exec(QStringLiteral( |
| 334 | "CREATE INDEX IF NOT EXISTS idx_mutation_journal_account_phase " | ||
| 335 | "ON mutation_journal(account_id, phase, token)")); | ||
| 336 |
1/1✓ Branch 35 → 36 taken 1670 times.
|
1670 | q.exec(QStringLiteral( |
| 337 | "CREATE INDEX IF NOT EXISTS idx_mutation_journal_identity " | ||
| 338 | "ON mutation_journal(account_id, folder_id, uid, flag_name)")); | ||
| 339 | // Page reads exclude pending moves per row (headerPageSelection). | ||
| 340 |
1/1✓ Branch 41 → 42 taken 1670 times.
|
1670 | q.exec(QStringLiteral( |
| 341 | "CREATE INDEX IF NOT EXISTS idx_mutation_journal_folder_uid " | ||
| 342 | "ON mutation_journal(folder_id, uid)")); | ||
| 343 | |||
| 344 |
2/3✓ Branch 44 → 45 taken 1670 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 52 taken 1670 times.
|
1670 | if (!q.exec(AppendPayloadSchema)) { |
| 345 | ✗ | m_lastError = q.lastError().text(); | |
| 346 | ✗ | return false; | |
| 347 | } | ||
| 348 | 1670 | return true; | |
| 349 | 1670 | } | |
| 350 | |||
| 351 | // createSchema step: whitelist, cache_meta and the payload byte-counter triggers. | ||
| 352 | 1670 | bool MailCacheSchema::createMetaTables() { | |
| 353 |
1/1✓ Branch 2 → 3 taken 1670 times.
|
1670 | QSqlQuery q(m_db); |
| 354 | 1670 | bool ok = false; | |
| 355 | // T-122: External content whitelist | ||
| 356 |
1/1✓ Branch 6 → 7 taken 1670 times.
|
1670 | ok = q.exec(QStringLiteral( |
| 357 | "CREATE TABLE IF NOT EXISTS external_content_whitelist (" | ||
| 358 | " id INTEGER PRIMARY KEY AUTOINCREMENT," | ||
| 359 | " type TEXT NOT NULL CHECK(type IN ('sender','domain'))," | ||
| 360 | " value TEXT NOT NULL," | ||
| 361 | " created_at TEXT DEFAULT (datetime('now'))," | ||
| 362 | " UNIQUE(type, value)" | ||
| 363 | ")")); | ||
| 364 |
2/2✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 26 taken 1669 times.
|
1670 | if (!ok) { |
| 365 |
5/5✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 24 → 12 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to create whitelist table:" |
| 366 |
3/3✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
1 | << q.lastError().text(); |
| 367 | 1 | return false; | |
| 368 | } | ||
| 369 | |||
| 370 | // SEC-2026-07-21-23: cache_meta table for crash-safe FTS rebuild watermark. | ||
| 371 |
1/1✓ Branch 29 → 30 taken 1669 times.
|
1669 | ok = q.exec(QStringLiteral("CREATE TABLE IF NOT EXISTS cache_meta (" |
| 372 | " key TEXT PRIMARY KEY," | ||
| 373 | " value TEXT" | ||
| 374 | ")")); | ||
| 375 |
1/2✗ Branch 32 → 33 not taken.
✓ Branch 32 → 49 taken 1669 times.
|
1669 | if (!ok) { |
| 376 | ✗ | qCWarning(lcCache) << "Failed to create cache_meta table:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; schema result remains measured. | |
| 377 | ✗ | << q.lastError().text(); | |
| 378 | ✗ | return false; | |
| 379 | } | ||
| 380 | |||
| 381 |
1/1✓ Branch 52 → 53 taken 1669 times.
|
1669 | ok = q.exec(QStringLiteral( |
| 382 | "INSERT OR IGNORE INTO cache_meta(key, value) SELECT " | ||
| 383 | "'payload_cache_bytes', CAST(" | ||
| 384 | "COALESCE((SELECT SUM(COALESCE(length(CAST(text_plain AS BLOB)), 0) + " | ||
| 385 | "COALESCE(length(CAST(text_html AS BLOB)), 0) + " | ||
| 386 | "COALESCE(length(raw_body), 0)) FROM bodies), 0) + " | ||
| 387 | "COALESCE((SELECT SUM(COALESCE(length(data), 0)) FROM attachments), 0) " | ||
| 388 | "AS TEXT) WHERE NOT EXISTS " | ||
| 389 | "(SELECT 1 FROM cache_meta WHERE key='payload_cache_bytes')")); | ||
| 390 |
2/2✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 72 taken 1668 times.
|
1669 | if (!ok) { |
| 391 |
5/5✓ Branch 56 → 57 taken 1 time.
✓ Branch 60 → 61 taken 1 time.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 70 → 58 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
|
2 | qCWarning(lcCache) << "Failed to initialize payload byte counter:" |
| 392 |
3/3✓ Branch 62 → 63 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
|
1 | << q.lastError().text(); |
| 393 | 1 | return false; | |
| 394 | } | ||
| 395 | |||
| 396 | 1668 | const QString bodyBytes = QStringLiteral( | |
| 397 | "COALESCE(length(CAST(%1.text_plain AS BLOB)), 0) + " | ||
| 398 | "COALESCE(length(CAST(%1.text_html AS BLOB)), 0) + " | ||
| 399 | "COALESCE(length(%1.raw_body), 0)"); | ||
| 400 | const QStringList payloadTriggers = { | ||
| 401 | 3336 | QStringLiteral( | |
| 402 | "CREATE TRIGGER IF NOT EXISTS payload_bodies_ai AFTER INSERT ON " | ||
| 403 | "bodies BEGIN UPDATE cache_meta SET value = CAST(CAST(value AS " | ||
| 404 | "INTEGER) + (%1) AS TEXT) WHERE key='payload_cache_bytes'; END") | ||
| 405 |
1/1✓ Branch 84 → 85 taken 1668 times.
|
3336 | .arg(bodyBytes.arg(QStringLiteral("new"))), |
| 406 | 3336 | QStringLiteral( | |
| 407 | "CREATE TRIGGER IF NOT EXISTS payload_bodies_ad AFTER DELETE ON " | ||
| 408 | "bodies BEGIN UPDATE cache_meta SET value = CAST(CAST(value AS " | ||
| 409 | "INTEGER) - (%1) AS TEXT) WHERE key='payload_cache_bytes'; END") | ||
| 410 |
1/1✓ Branch 94 → 95 taken 1668 times.
|
3336 | .arg(bodyBytes.arg(QStringLiteral("old"))), |
| 411 | 3336 | QStringLiteral( | |
| 412 | "CREATE TRIGGER IF NOT EXISTS payload_bodies_au AFTER UPDATE ON " | ||
| 413 | "bodies BEGIN UPDATE cache_meta SET value = CAST(CAST(value AS " | ||
| 414 | "INTEGER) + (%1) - (%2) AS TEXT) WHERE " | ||
| 415 | "key='payload_cache_bytes'; END") | ||
| 416 |
1/1✓ Branch 108 → 109 taken 1668 times.
|
3336 | .arg(bodyBytes.arg(QStringLiteral("new")), |
| 417 |
1/1✓ Branch 103 → 104 taken 1668 times.
|
3336 | bodyBytes.arg(QStringLiteral("old"))), |
| 418 | 1668 | QStringLiteral( | |
| 419 | "CREATE TRIGGER IF NOT EXISTS payload_attachments_ai AFTER INSERT " | ||
| 420 | "ON attachments BEGIN UPDATE cache_meta SET value = " | ||
| 421 | "CAST(CAST(value AS INTEGER) + COALESCE(length(new.data), 0) AS " | ||
| 422 | "TEXT) WHERE key='payload_cache_bytes'; END"), | ||
| 423 | 1668 | QStringLiteral( | |
| 424 | "CREATE TRIGGER IF NOT EXISTS payload_attachments_ad AFTER DELETE " | ||
| 425 | "ON attachments BEGIN UPDATE cache_meta SET value = " | ||
| 426 | "CAST(CAST(value AS INTEGER) - COALESCE(length(old.data), 0) AS " | ||
| 427 | "TEXT) WHERE key='payload_cache_bytes'; END"), | ||
| 428 | 1668 | QStringLiteral( | |
| 429 | "CREATE TRIGGER IF NOT EXISTS payload_attachments_au AFTER UPDATE " | ||
| 430 | "ON attachments BEGIN UPDATE cache_meta SET value = " | ||
| 431 | "CAST(CAST(value AS INTEGER) + COALESCE(length(new.data), 0) - " | ||
| 432 | "COALESCE(length(old.data), 0) AS TEXT) WHERE " | ||
| 433 | "key='payload_cache_bytes'; END"), | ||
| 434 |
2/4✓ Branch 121 → 122 taken 10008 times.
✓ Branch 121 → 123 taken 1668 times.
✗ Branch 226 → 227 not taken.
✗ Branch 226 → 228 not taken.
|
11676 | }; |
| 435 |
2/2✓ Branch 167 → 146 taken 9993 times.
✓ Branch 167 → 168 taken 1662 times.
|
11655 | for (const QString &trigger : payloadTriggers) { |
| 436 |
3/3✓ Branch 147 → 148 taken 9993 times.
✓ Branch 148 → 149 taken 6 times.
✓ Branch 148 → 165 taken 9987 times.
|
9993 | if (!q.exec(trigger)) { |
| 437 |
5/5✓ Branch 149 → 150 taken 6 times.
✓ Branch 153 → 154 taken 6 times.
✓ Branch 154 → 155 taken 6 times.
✓ Branch 163 → 151 taken 6 times.
✓ Branch 163 → 164 taken 6 times.
|
12 | qCWarning(lcCache) << "Failed to create payload accounting trigger:" |
| 438 |
3/3✓ Branch 155 → 156 taken 6 times.
✓ Branch 156 → 157 taken 6 times.
✓ Branch 157 → 158 taken 6 times.
|
6 | << q.lastError().text(); |
| 439 | 6 | return false; | |
| 440 | } | ||
| 441 | } | ||
| 442 | 1662 | return true; | |
| 443 | 16682 | } | |
| 444 | |||
| 445 | // createSchema step: the search projection, the FTS5 index and its triggers. | ||
| 446 | 1662 | bool MailCacheSchema::createSearchTables() { | |
| 447 |
1/1✓ Branch 2 → 3 taken 1662 times.
|
1662 | QSqlQuery q(m_db); |
| 448 | 1662 | bool ok = false; | |
| 449 | // Canonical folded projection for search. FTS uses this as external content | ||
| 450 | // so the virtual table does not retain another complete copy of message | ||
| 451 | // fields. Writers update this row in the same transaction as header/body | ||
| 452 | // durability and the triggers apply exactly one index replacement. | ||
| 453 |
1/1✓ Branch 6 → 7 taken 1662 times.
|
1662 | ok = q.exec(QStringLiteral( |
| 454 | "CREATE TABLE IF NOT EXISTS search_documents (" | ||
| 455 | " header_id INTEGER PRIMARY KEY REFERENCES headers(id) ON DELETE CASCADE," | ||
| 456 | " subject TEXT NOT NULL DEFAULT ''," | ||
| 457 | " from_addr TEXT NOT NULL DEFAULT ''," | ||
| 458 | " to_addr TEXT NOT NULL DEFAULT ''," | ||
| 459 | " body_text TEXT NOT NULL DEFAULT ''" | ||
| 460 | ")")); | ||
| 461 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 26 taken 1662 times.
|
1662 | if (!ok) { |
| 462 | ✗ | qCWarning(lcCache) << "Failed to create search document table:" | |
| 463 | ✗ | << q.lastError().text(); | |
| 464 | ✗ | return false; | |
| 465 | } | ||
| 466 | |||
| 467 | // T-179: FTS5 full-text search index on cached emails. | ||
| 468 | // Uses the trigram tokenizer for substring matching ANYWHERE in a word | ||
| 469 | // (e.g. "rechnung" matches "Stromrechnung") — important for German compound | ||
| 470 | // words. Diacritic insensitivity is handled by folding the text ourselves | ||
| 471 | // (foldForSearch) before it is stored, because trigram's remove_diacritics | ||
| 472 | // option only exists in SQLite >= 3.45. Trigram requires search terms of at | ||
| 473 | // least 3 characters. | ||
| 474 |
1/1✓ Branch 29 → 30 taken 1662 times.
|
1662 | ok = q.exec(QStringLiteral( |
| 475 | "CREATE VIRTUAL TABLE IF NOT EXISTS mail_fts USING fts5(" | ||
| 476 | " subject, from_addr, to_addr, body_text," | ||
| 477 | " content='search_documents', content_rowid='header_id'," | ||
| 478 | " columnsize=0," | ||
| 479 | " tokenize = 'trigram'" | ||
| 480 | ")")); | ||
| 481 | 1662 | const bool ftsAvailable = ok; | |
| 482 |
2/2✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 49 taken 1660 times.
|
1662 | if (!ok) { |
| 483 |
5/5✓ Branch 33 → 34 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 38 → 39 taken 2 times.
✓ Branch 47 → 35 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
|
4 | qCWarning(lcCache) << "Failed to create FTS5 table:" |
| 484 |
3/3✓ Branch 39 → 40 taken 2 times.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 41 → 42 taken 2 times.
|
2 | << q.lastError().text(); |
| 485 | // Non-fatal: search will be unavailable but core functionality works | ||
| 486 | } | ||
| 487 | |||
| 488 |
1/1✓ Branch 52 → 53 taken 1662 times.
|
1662 | ok = q.exec(QStringLiteral( |
| 489 | "INSERT OR IGNORE INTO cache_meta(key,value) SELECT " | ||
| 490 | "'fts_indexed_documents', CAST(CASE WHEN EXISTS(" | ||
| 491 | "SELECT 1 FROM cache_meta WHERE key='fts_rebuild_in_progress') " | ||
| 492 | "THEN 0 ELSE (SELECT COUNT(*) FROM search_documents) END AS TEXT)")); | ||
| 493 |
1/2✗ Branch 55 → 56 not taken.
✓ Branch 55 → 72 taken 1662 times.
|
1662 | if (!ok) { |
| 494 | ✗ | qCWarning(lcCache) << "Failed to initialize FTS document counter:" | |
| 495 | ✗ | << q.lastError().text(); | |
| 496 | ✗ | return false; | |
| 497 | } | ||
| 498 | |||
| 499 | const QStringList searchTriggers = { | ||
| 500 | ✗ | QStringLiteral( | |
| 501 | "CREATE TRIGGER IF NOT EXISTS search_documents_ai AFTER INSERT ON " | ||
| 502 | "search_documents BEGIN INSERT INTO mail_fts(rowid, subject, " | ||
| 503 | "from_addr, to_addr, body_text) VALUES (new.header_id, new.subject, " | ||
| 504 | "new.from_addr, new.to_addr, new.body_text); UPDATE cache_meta SET " | ||
| 505 | "value=CAST(CAST(value AS INTEGER)+1 AS TEXT) WHERE " | ||
| 506 | "key='fts_indexed_documents'; END"), | ||
| 507 | 1662 | QStringLiteral( | |
| 508 | "CREATE TRIGGER IF NOT EXISTS search_documents_ad AFTER DELETE ON " | ||
| 509 | "search_documents BEGIN INSERT INTO mail_fts(mail_fts, rowid, " | ||
| 510 | "subject, from_addr, to_addr, body_text) VALUES ('delete', " | ||
| 511 | "old.header_id, old.subject, old.from_addr, old.to_addr, " | ||
| 512 | "old.body_text); UPDATE cache_meta SET value=CAST(MAX(0, " | ||
| 513 | "CAST(value AS INTEGER)-1) AS TEXT) WHERE " | ||
| 514 | "key='fts_indexed_documents'; END"), | ||
| 515 | 1662 | QStringLiteral( | |
| 516 | "CREATE TRIGGER IF NOT EXISTS search_documents_au AFTER UPDATE ON " | ||
| 517 | "search_documents BEGIN INSERT INTO mail_fts(mail_fts, rowid, " | ||
| 518 | "subject, from_addr, to_addr, body_text) VALUES ('delete', " | ||
| 519 | "old.header_id, old.subject, old.from_addr, old.to_addr, " | ||
| 520 | "old.body_text); INSERT INTO mail_fts(rowid, subject, from_addr, " | ||
| 521 | "to_addr, body_text) VALUES (new.header_id, new.subject, " | ||
| 522 | "new.from_addr, new.to_addr, new.body_text); END"), | ||
| 523 |
2/4✓ Branch 83 → 84 taken 4986 times.
✓ Branch 83 → 85 taken 1662 times.
✗ Branch 203 → 204 not taken.
✗ Branch 203 → 205 not taken.
|
6648 | }; |
| 524 |
2/2✓ Branch 88 → 89 taken 2 times.
✓ Branch 88 → 122 taken 1660 times.
|
1662 | if (!ftsAvailable) { |
| 525 | // S106-111: triggers into a missing mail_fts would make every write to | ||
| 526 | // search_documents fail (SQLite resolves trigger bodies when they fire), | ||
| 527 | // so "non-fatal" holds only without them. | ||
| 528 |
2/2✓ Branch 120 → 91 taken 6 times.
✓ Branch 120 → 121 taken 2 times.
|
8 | for (const char *name : {"search_documents_ai", "search_documents_ad", "search_documents_au"}) { |
| 529 |
4/5✓ Branch 92 → 93 taken 6 times.
✓ Branch 96 → 97 taken 6 times.
✓ Branch 97 → 98 taken 6 times.
✗ Branch 102 → 103 not taken.
✓ Branch 102 → 119 taken 6 times.
|
12 | if (!q.exec(QStringLiteral("DROP TRIGGER IF EXISTS ") + QLatin1String(name))) { |
| 530 | ✗ | qCWarning(lcCache) << "Failed to drop search document trigger:" << q.lastError().text(); | |
| 531 | ✗ | return false; | |
| 532 | } | ||
| 533 | } | ||
| 534 | 2 | return true; | |
| 535 | } | ||
| 536 |
2/2✓ Branch 145 → 124 taken 4980 times.
✓ Branch 145 → 146 taken 1660 times.
|
6640 | for (const QString &trigger : searchTriggers) { |
| 537 |
2/3✓ Branch 125 → 126 taken 4980 times.
✗ Branch 126 → 127 not taken.
✓ Branch 126 → 143 taken 4980 times.
|
4980 | if (!q.exec(trigger)) { |
| 538 | ✗ | qCWarning(lcCache) << "Failed to create search document trigger:" | |
| 539 | ✗ | << q.lastError().text(); | |
| 540 | ✗ | return false; | |
| 541 | } | ||
| 542 | } | ||
| 543 | 1660 | return true; | |
| 544 | 8310 | } | |
| 545 | |||
| 546 | // Create database schema (tables + indexes); formerly MailCache::createSchema(). | ||
| 547 | 1694 | bool MailCacheSchema::createSchema() { | |
| 548 |
2/2✓ Branch 5 → 6 taken 1670 times.
✓ Branch 5 → 15 taken 3 times.
|
3367 | return createMessageTables() && createAttachmentAndLabelTables() && |
| 549 |
3/4✓ Branch 7 → 8 taken 1670 times.
✗ Branch 7 → 15 not taken.
✓ Branch 9 → 10 taken 1662 times.
✓ Branch 9 → 15 taken 8 times.
|
1670 | createMutationJournalTables() && createMetaTables() && |
| 550 |
4/6✓ Branch 3 → 4 taken 1673 times.
✓ Branch 3 → 15 taken 21 times.
✓ Branch 11 → 12 taken 1662 times.
✗ Branch 11 → 15 not taken.
✓ Branch 13 → 14 taken 1662 times.
✗ Branch 13 → 15 not taken.
|
3367 | createSearchTables() && ensureThreadProjectionSchema(m_db, &m_lastError); |
| 551 | } | ||
| 552 |