MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 95.7% 445 / 0 / 465
Functions: 100.0% 26 / 0 / 26
Branches: 84.4% 688 / 0 / 815

data/MutationJournalStore.cpp
Line Branch Exec Source
1 #include "data/MutationJournalStore.h"
2
3 #include "data/MailCacheSql.h"
4 #include <QCryptographicHash>
5 #include <QDateTime>
6 #include <QLoggingCategory>
7 #include <QScopeGuard>
8 #include <QSqlError>
9 #include <QSqlQuery>
10 #include <QVariant>
11
12 // Same category as MailCache (defined there), see docs/LOGGING.md.
13 Q_DECLARE_LOGGING_CATEGORY(lcCache)
14
15 using MailCacheSql::ScopedWriteUnit;
16 using MailCacheSql::beginWriteTransaction;
17
18 5035 MutationJournalStore::MutationJournalStore(QSqlDatabase &db, QString &lastError, Notify notify)
19 5035 : m_db(db), m_lastError(lastError), m_notify(std::move(notify)) {}
20
21 // --- Durable mutation journal (Sprint 84) ---
22
23 namespace {
24 constexpr auto MutationColumns =
25 "token, account_id, folder_id, folder_path, uid, kind, replay_class, "
26 "phase, flag_name, desired_state, target_folder, message_id, uidvalidity, "
27 "uidplus_evidence, error, created_at, updated_at";
28
29 2948614 MutationRecord mutationFromQuery(const QSqlQuery &query) {
30 2948614 MutationRecord record;
31
2/2
✓ Branch 2 → 3 taken 2948614 times.
✓ Branch 3 → 4 taken 2948614 times.
2948614 record.token = query.value(0).toLongLong();
32
2/2
✓ Branch 5 → 6 taken 2948614 times.
✓ Branch 6 → 7 taken 2948614 times.
2948614 record.mailId.accountId = query.value(1).toString();
33
2/2
✓ Branch 10 → 11 taken 2948614 times.
✓ Branch 11 → 12 taken 2948614 times.
2948614 record.mailId.folderId = query.value(2).toLongLong();
34
2/2
✓ Branch 13 → 14 taken 2948614 times.
✓ Branch 14 → 15 taken 2948614 times.
2948614 record.folderPath = query.value(3).toString();
35
2/2
✓ Branch 18 → 19 taken 2948614 times.
✓ Branch 19 → 20 taken 2948614 times.
2948614 record.mailId.uid = query.value(4).toLongLong();
36
2/2
✓ Branch 21 → 22 taken 2948614 times.
✓ Branch 22 → 23 taken 2948614 times.
2948614 record.kind = static_cast<MutationKind>(query.value(5).toInt());
37 2948614 record.replayClass =
38
2/2
✓ Branch 24 → 25 taken 2948614 times.
✓ Branch 25 → 26 taken 2948614 times.
2948614 static_cast<MutationReplayClass>(query.value(6).toInt());
39
2/2
✓ Branch 27 → 28 taken 2948614 times.
✓ Branch 28 → 29 taken 2948614 times.
2948614 record.phase = static_cast<MutationPhase>(query.value(7).toInt());
40
2/2
✓ Branch 30 → 31 taken 2948614 times.
✓ Branch 31 → 32 taken 2948614 times.
2948614 record.flag = query.value(8).toString();
41
2/2
✓ Branch 35 → 36 taken 2948614 times.
✓ Branch 36 → 37 taken 2948614 times.
2948614 record.desiredState = query.value(9).toBool();
42
2/2
✓ Branch 38 → 39 taken 2948614 times.
✓ Branch 39 → 40 taken 2948614 times.
2948614 record.targetFolder = query.value(10).toString();
43
2/2
✓ Branch 43 → 44 taken 2948614 times.
✓ Branch 44 → 45 taken 2948614 times.
2948614 record.messageId = query.value(11).toString();
44
2/2
✓ Branch 48 → 49 taken 2948614 times.
✓ Branch 49 → 50 taken 2948614 times.
2948614 record.uidValidity = query.value(12).toUInt();
45
2/2
✓ Branch 51 → 52 taken 2948614 times.
✓ Branch 52 → 53 taken 2948614 times.
2948614 record.uidPlusEvidence = query.value(13).toString();
46
2/2
✓ Branch 56 → 57 taken 2948614 times.
✓ Branch 57 → 58 taken 2948614 times.
2948614 record.error = query.value(14).toString();
47
2/2
✓ Branch 61 → 62 taken 2948614 times.
✓ Branch 62 → 63 taken 2948614 times.
2948614 record.createdAt = query.value(15).toLongLong();
48
2/2
✓ Branch 64 → 65 taken 2948614 times.
✓ Branch 65 → 66 taken 2948614 times.
2948614 record.updatedAt = query.value(16).toLongLong();
49 2948614 return record;
50 ✗ }
51
52 6408 qint64 estimatedMutationBytes(const MutationRecord &record) {
53 6408 return 128 + 2LL *
54 6408 (record.mailId.accountId.size() + record.folderPath.size() +
55 6408 record.flag.size() + record.targetFolder.size() +
56 6408 record.messageId.size() + record.uidPlusEvidence.size() +
57 6408 record.error.size());
58 }
59 } // namespace
60
61 117 QSet<qint64> MutationJournalStore::pendingMoveUids(const QString &account, qint64 folderId,
62 bool *ok) const {
63
2/2
✓ Branch 2 → 3 taken 57 times.
✓ Branch 2 → 4 taken 60 times.
117 if (ok) *ok = false;
64
1/1
✓ Branch 4 → 5 taken 117 times.
117 QSqlQuery q(m_db);
65
1/1
✓ Branch 8 → 9 taken 117 times.
117 q.prepare(QStringLiteral(
66 "SELECT uid FROM mutation_journal WHERE account_id=:account AND folder_id=:fid "
67 "AND kind=:move AND phase NOT IN (:confirmed, :failed, :quarantined)"));
68
1/1
✓ Branch 16 → 17 taken 117 times.
234 q.bindValue(QStringLiteral(":account"), account);
69
1/1
✓ Branch 25 → 26 taken 117 times.
234 q.bindValue(QStringLiteral(":fid"), folderId);
70
1/1
✓ Branch 34 → 35 taken 117 times.
234 q.bindValue(QStringLiteral(":move"), int(MutationKind::Move));
71
1/1
✓ Branch 43 → 44 taken 117 times.
234 q.bindValue(QStringLiteral(":confirmed"), int(MutationPhase::Confirmed));
72
1/1
✓ Branch 52 → 53 taken 117 times.
234 q.bindValue(QStringLiteral(":failed"), int(MutationPhase::Failed));
73
1/1
✓ Branch 61 → 62 taken 117 times.
234 q.bindValue(QStringLiteral(":quarantined"), int(MutationPhase::Quarantined));
74 117 QSet<qint64> result;
75
2/3
✓ Branch 66 → 67 taken 117 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 69 taken 117 times.
117 if (!q.exec()) return result;
76
6/6
✓ Branch 70 → 71 taken 33 times.
✓ Branch 71 → 72 taken 33 times.
✓ Branch 72 → 73 taken 33 times.
✓ Branch 75 → 76 taken 150 times.
✓ Branch 76 → 70 taken 33 times.
✓ Branch 76 → 77 taken 117 times.
150 while (q.next()) result.insert(q.value(0).toLongLong());
77
4/4
✓ Branch 77 → 78 taken 57 times.
✓ Branch 77 → 82 taken 60 times.
✓ Branch 78 → 79 taken 57 times.
✓ Branch 79 → 80 taken 57 times.
117 if (ok) *ok = !q.lastError().isValid();
78 117 return result;
79 117 }
80
81 14 bool MutationJournalStore::hasPendingSeenMutation(const QString &account, qint64 folderId) const {
82
1/1
✓ Branch 2 → 3 taken 14 times.
14 QSqlQuery q(m_db);
83
1/1
✓ Branch 6 → 7 taken 14 times.
14 q.prepare(QStringLiteral("SELECT 1 FROM mutation_journal WHERE account_id=? "
84 "AND folder_id=? AND kind=? AND phase IN (?,?,?) LIMIT 1"));
85
1/1
✓ Branch 11 → 12 taken 14 times.
14 q.addBindValue(account);
86
1/1
✓ Branch 15 → 16 taken 14 times.
14 q.addBindValue(folderId);
87
1/1
✓ Branch 19 → 20 taken 14 times.
14 q.addBindValue(int(MutationKind::Seen));
88
1/1
✓ Branch 23 → 24 taken 14 times.
14 q.addBindValue(int(MutationPhase::Queued));
89
1/1
✓ Branch 27 → 28 taken 14 times.
14 q.addBindValue(int(MutationPhase::Dispatched));
90
1/1
✓ Branch 31 → 32 taken 14 times.
14 q.addBindValue(int(MutationPhase::AwaitingVerification));
91
5/6
✓ Branch 33 → 34 taken 14 times.
✓ Branch 34 → 35 taken 14 times.
✗ Branch 34 → 38 not taken.
✓ Branch 35 → 36 taken 14 times.
✓ Branch 36 → 37 taken 3 times.
✓ Branch 36 → 38 taken 11 times.
28 return q.exec() && q.next();
92 14 }
93
94 std::optional<qint64>
95 390 MutationJournalStore::enqueueMutation(const MutationRecord &record) {
96
5/5
✓ Branch 2 → 3 taken 390 times.
✓ Branch 5 → 6 taken 388 times.
✓ Branch 5 → 20 taken 1 time.
✓ Branch 7 → 8 taken 387 times.
✓ Branch 7 → 20 taken 1 time.
1167 if (!m_db.isOpen() || !record.isValid() ||
97
4/4
✓ Branch 9 → 10 taken 386 times.
✓ Branch 9 → 20 taken 1 time.
✓ Branch 11 → 12 taken 385 times.
✓ Branch 11 → 20 taken 1 time.
1161 record.mailId.accountId.size() > 256 || record.folderPath.size() > 1024 ||
98
4/4
✓ Branch 13 → 14 taken 384 times.
✓ Branch 13 → 20 taken 1 time.
✓ Branch 15 → 16 taken 383 times.
✓ Branch 15 → 20 taken 1 time.
1155 record.flag.size() > 256 || record.targetFolder.size() > 1024 ||
99
2/2
✓ Branch 17 → 18 taken 382 times.
✓ Branch 17 → 20 taken 1 time.
767 record.messageId.size() > 2048 ||
100
6/6
✓ Branch 3 → 4 taken 389 times.
✓ Branch 3 → 20 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 381 times.
✓ Branch 22 → 23 taken 9 times.
✓ Branch 22 → 32 taken 381 times.
1162 record.uidPlusEvidence.size() > 1024 || record.error.size() > 2048) {
101 9 m_lastError = QStringLiteral("Invalid or oversized mutation journal entry");
102 9 return std::nullopt;
103 }
104
105
1/1
✓ Branch 32 → 33 taken 381 times.
381 ScopedWriteUnit unit(m_db, &m_lastError);
106
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 38 taken 381 times.
381 if (!unit.started()) return std::nullopt;
107
108 381 const qint64 now = QDateTime::currentSecsSinceEpoch();
109
2/2
✓ Branch 39 → 40 taken 348 times.
✓ Branch 39 → 212 taken 33 times.
381 if (record.replayClass == MutationReplayClass::DesiredState) {
110
1/1
✓ Branch 40 → 41 taken 348 times.
348 QSqlQuery existing(m_db);
111
1/1
✓ Branch 44 → 45 taken 348 times.
348 existing.prepare(QStringLiteral(
112 "SELECT token FROM mutation_journal WHERE account_id = :account "
113 "AND folder_id = :folder AND uid = :uid AND kind = :kind "
114 "AND flag_name = :flag AND replay_class = :class "
115 "AND phase = :queued "
116 "ORDER BY token DESC LIMIT 1"));
117
1/1
✓ Branch 52 → 53 taken 348 times.
696 existing.bindValue(QStringLiteral(":account"), record.mailId.accountId);
118
1/1
✓ Branch 61 → 62 taken 348 times.
696 existing.bindValue(QStringLiteral(":folder"), record.mailId.folderId);
119
1/1
✓ Branch 70 → 71 taken 348 times.
696 existing.bindValue(QStringLiteral(":uid"), record.mailId.uid);
120
1/1
✓ Branch 79 → 80 taken 348 times.
696 existing.bindValue(QStringLiteral(":kind"), static_cast<int>(record.kind));
121
1/1
✓ Branch 88 → 89 taken 348 times.
696 existing.bindValue(QStringLiteral(":flag"), record.flag);
122
1/1
✓ Branch 97 → 98 taken 348 times.
696 existing.bindValue(QStringLiteral(":class"),
123 static_cast<int>(MutationReplayClass::DesiredState));
124
1/1
✓ Branch 106 → 107 taken 348 times.
696 existing.bindValue(QStringLiteral(":queued"),
125 static_cast<int>(MutationPhase::Queued));
126
3/3
✓ Branch 110 → 111 taken 348 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 120 taken 347 times.
348 if (!existing.exec()) {
127
2/2
✓ Branch 112 → 113 taken 1 time.
✓ Branch 113 → 114 taken 1 time.
1 m_lastError = existing.lastError().text();
128 1 return std::nullopt;
129 }
130
3/3
✓ Branch 120 → 121 taken 347 times.
✓ Branch 121 → 122 taken 62 times.
✓ Branch 121 → 206 taken 285 times.
347 if (existing.next()) {
131
2/2
✓ Branch 122 → 123 taken 62 times.
✓ Branch 123 → 124 taken 62 times.
62 const qint64 token = existing.value(0).toLongLong();
132
1/1
✓ Branch 125 → 126 taken 62 times.
62 QSqlQuery update(m_db);
133
1/1
✓ Branch 129 → 130 taken 62 times.
62 update.prepare(QStringLiteral(
134 "UPDATE mutation_journal SET desired_state = :desired, "
135 "folder_path = :path, uidvalidity = :validity, phase = :phase, "
136 "error = '', uidplus_evidence = '', updated_at = :updated "
137 "WHERE token = :token"));
138
1/1
✓ Branch 137 → 138 taken 62 times.
124 update.bindValue(QStringLiteral(":desired"), record.desiredState);
139
1/1
✓ Branch 146 → 147 taken 62 times.
124 update.bindValue(QStringLiteral(":path"), record.folderPath);
140
1/1
✓ Branch 155 → 156 taken 62 times.
124 update.bindValue(QStringLiteral(":validity"), record.uidValidity);
141
1/1
✓ Branch 164 → 165 taken 62 times.
124 update.bindValue(QStringLiteral(":phase"),
142 static_cast<int>(MutationPhase::Queued));
143
1/1
✓ Branch 173 → 174 taken 62 times.
124 update.bindValue(QStringLiteral(":updated"), now);
144
1/1
✓ Branch 182 → 183 taken 62 times.
124 update.bindValue(QStringLiteral(":token"), token);
145
3/3
✓ Branch 186 → 187 taken 62 times.
✓ Branch 187 → 188 taken 1 time.
✓ Branch 187 → 196 taken 61 times.
62 if (!update.exec()) {
146
2/2
✓ Branch 188 → 189 taken 1 time.
✓ Branch 189 → 190 taken 1 time.
1 m_lastError = update.lastError().text();
147 1 return std::nullopt;
148 }
149
2/3
✓ Branch 196 → 197 taken 61 times.
✗ Branch 197 → 198 not taken.
✓ Branch 197 → 201 taken 61 times.
61 if (!unit.commit(&m_lastError)) return std::nullopt;
150 61 return token;
151 62 }
152 348 }
153
154 // S106-115: fail closed like the batch path — an unreadable usage is no
155 // free capacity.
156 318 int count = 0;
157 318 qint64 used = 0;
158
2/3
✓ Branch 212 → 213 taken 318 times.
✗ Branch 213 → 214 not taken.
✓ Branch 213 → 223 taken 318 times.
318 if (!journalUsage(&count, &used)) {
159 ✗ m_lastError = QStringLiteral("Mutation journal usage could not be read");
160 ✗ return std::nullopt;
161 }
162
4/4
✓ Branch 223 → 224 taken 317 times.
✓ Branch 223 → 226 taken 1 time.
✓ Branch 228 → 229 taken 2 times.
✓ Branch 228 → 238 taken 316 times.
635 if (count >= m_mutationJournalMaxEntries ||
163
2/2
✓ Branch 225 → 226 taken 1 time.
✓ Branch 225 → 227 taken 316 times.
317 estimatedMutationBytes(record) > m_mutationJournalMaxBytes - used) {
164 2 m_lastError = QStringLiteral("Mutation journal capacity exceeded");
165 2 return std::nullopt;
166 }
167
168
1/1
✓ Branch 238 → 239 taken 316 times.
316 QSqlQuery insert(m_db);
169
1/1
✓ Branch 242 → 243 taken 316 times.
316 insert.prepare(QStringLiteral(
170 "INSERT INTO mutation_journal (account_id, folder_id, folder_path, uid, "
171 "kind, replay_class, phase, flag_name, desired_state, target_folder, "
172 "message_id, uidvalidity, uidplus_evidence, error, created_at, updated_at) "
173 "VALUES (:account, :folder, :path, :uid, :kind, :class, :phase, :flag, "
174 ":desired, :target, :message, :validity, :uidplus, :error, :created, "
175 ":updated)"));
176
1/1
✓ Branch 250 → 251 taken 316 times.
632 insert.bindValue(QStringLiteral(":account"), record.mailId.accountId);
177
1/1
✓ Branch 259 → 260 taken 316 times.
632 insert.bindValue(QStringLiteral(":folder"), record.mailId.folderId);
178
1/1
✓ Branch 268 → 269 taken 316 times.
632 insert.bindValue(QStringLiteral(":path"), record.folderPath);
179
1/1
✓ Branch 277 → 278 taken 316 times.
632 insert.bindValue(QStringLiteral(":uid"), record.mailId.uid);
180
1/1
✓ Branch 286 → 287 taken 316 times.
632 insert.bindValue(QStringLiteral(":kind"), static_cast<int>(record.kind));
181
1/1
✓ Branch 295 → 296 taken 316 times.
316 insert.bindValue(QStringLiteral(":class"),
182 316 static_cast<int>(record.replayClass));
183
1/1
✓ Branch 304 → 305 taken 316 times.
632 insert.bindValue(QStringLiteral(":phase"), static_cast<int>(record.phase));
184
1/1
✓ Branch 313 → 314 taken 316 times.
632 insert.bindValue(QStringLiteral(":flag"), record.flag);
185
1/1
✓ Branch 322 → 323 taken 316 times.
632 insert.bindValue(QStringLiteral(":desired"), record.desiredState);
186
1/1
✓ Branch 331 → 332 taken 316 times.
632 insert.bindValue(QStringLiteral(":target"), record.targetFolder);
187
1/1
✓ Branch 340 → 341 taken 316 times.
632 insert.bindValue(QStringLiteral(":message"), record.messageId);
188
1/1
✓ Branch 349 → 350 taken 316 times.
632 insert.bindValue(QStringLiteral(":validity"), record.uidValidity);
189
1/1
✓ Branch 358 → 359 taken 316 times.
632 insert.bindValue(QStringLiteral(":uidplus"), record.uidPlusEvidence);
190
1/1
✓ Branch 367 → 368 taken 316 times.
632 insert.bindValue(QStringLiteral(":error"), record.error);
191
1/1
✓ Branch 376 → 377 taken 316 times.
632 insert.bindValue(QStringLiteral(":created"), now);
192
1/1
✓ Branch 385 → 386 taken 316 times.
632 insert.bindValue(QStringLiteral(":updated"), now);
193
3/3
✓ Branch 389 → 390 taken 316 times.
✓ Branch 390 → 391 taken 2 times.
✓ Branch 390 → 399 taken 314 times.
316 if (!insert.exec()) {
194
2/2
✓ Branch 391 → 392 taken 2 times.
✓ Branch 392 → 393 taken 2 times.
2 m_lastError = insert.lastError().text();
195 2 return std::nullopt;
196 }
197
2/2
✓ Branch 399 → 400 taken 314 times.
✓ Branch 400 → 401 taken 314 times.
314 const qint64 token = insert.lastInsertId().toLongLong();
198
1/2
✗ Branch 402 → 403 not taken.
✓ Branch 402 → 412 taken 314 times.
314 if (token <= 0) {
199 ✗ m_lastError = QStringLiteral("Mutation journal did not return a token");
200 ✗ return std::nullopt;
201 }
202
2/3
✓ Branch 412 → 413 taken 314 times.
✗ Branch 413 → 414 not taken.
✓ Branch 413 → 417 taken 314 times.
314 if (!unit.commit(&m_lastError)) return std::nullopt;
203 314 return token;
204 381 }
205
206 20 std::optional<qint64> MutationJournalStore::enqueueAppendMutation(const MutationRecord &record,
207 const QByteArray &message, const QString &flags,
208 const QByteArray &precomputedSha256) {
209
5/7
✓ Branch 2 → 3 taken 20 times.
✓ Branch 4 → 5 taken 20 times.
✗ Branch 4 → 26 not taken.
✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 26 taken 1 time.
✓ Branch 8 → 9 taken 19 times.
✗ Branch 8 → 26 not taken.
59 if (!m_db.isOpen() || record.kind != MutationKind::Append || message.isEmpty() ||
210
1/2
✓ Branch 10 → 11 taken 19 times.
✗ Branch 10 → 26 not taken.
38 message.size() > MaximumAppendMessageBytes || flags.size() > 256 ||
211
3/4
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 15 taken 14 times.
✓ Branch 14 → 15 taken 5 times.
✗ Branch 14 → 26 not taken.
19 (!precomputedSha256.isEmpty() && precomputedSha256.size() != 32) ||
212
10/13
✓ Branch 3 → 4 taken 20 times.
✗ Branch 3 → 26 not taken.
✓ Branch 17 → 18 taken 19 times.
✓ Branch 18 → 19 taken 18 times.
✓ Branch 18 → 26 taken 1 time.
✓ Branch 21 → 22 taken 18 times.
✓ Branch 22 → 23 taken 18 times.
✗ Branch 22 → 26 not taken.
✓ Branch 24 → 25 taken 18 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 18 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 38 taken 18 times.
40 flags.contains(QLatin1Char('\r')) || flags.contains(QLatin1Char('\n')) || flags.contains(QChar::Null)) {
213 2 m_lastError = QStringLiteral("Invalid or oversized append payload");
214 2 return std::nullopt;
215 }
216
2/3
✓ Branch 38 → 39 taken 18 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 43 taken 18 times.
18 if (!beginWriteTransaction(m_db, &m_lastError)) return std::nullopt;
217 21 auto rollback = qScopeGuard([this] { m_db.rollback(); });
218
1/1
✓ Branch 44 → 45 taken 18 times.
18 QSqlQuery capacity(m_db);
219
5/13
✓ Branch 48 → 49 taken 18 times.
✓ Branch 49 → 50 taken 18 times.
✗ Branch 49 → 52 not taken.
✓ Branch 54 → 55 taken 18 times.
✗ Branch 54 → 56 not taken.
✓ Branch 56 → 57 taken 18 times.
✗ Branch 56 → 58 not taken.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 67 taken 18 times.
✗ Branch 184 → 185 not taken.
✗ Branch 184 → 186 not taken.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
54 if (!capacity.exec(QStringLiteral("SELECT COALESCE(SUM(length(message)), 0) FROM append_payloads")) ||
220
2/3
✓ Branch 50 → 51 taken 18 times.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 53 taken 18 times.
18 !capacity.next()) {
221 ✗ m_lastError = capacity.lastError().text();
222 ✗ return std::nullopt;
223 }
224
4/4
✓ Branch 67 → 68 taken 18 times.
✓ Branch 68 → 69 taken 18 times.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 81 taken 17 times.
18 if (capacity.value(0).toLongLong() > MaximumPendingAppendBytes - message.size()) {
225 1 m_lastError = QStringLiteral("Pending message copies exceed the storage limit");
226 1 return std::nullopt;
227 }
228
3/5
✓ Branch 83 → 84 taken 17 times.
✓ Branch 86 → 87 taken 17 times.
✓ Branch 86 → 88 taken 17 times.
✗ Branch 202 → 203 not taken.
✗ Branch 202 → 204 not taken.
34 const auto tokens = enqueueMutationsInTransaction({record});
229
5/6
✓ Branch 89 → 90 taken 16 times.
✓ Branch 89 → 93 taken 1 time.
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 94 taken 16 times.
✓ Branch 95 → 96 taken 1 time.
✓ Branch 95 → 99 taken 16 times.
17 if (!tokens || tokens->size() != 1) return std::nullopt;
230
1/1
✓ Branch 99 → 100 taken 16 times.
16 QSqlQuery insert(m_db);
231
1/1
✓ Branch 103 → 104 taken 16 times.
16 insert.prepare(QStringLiteral("INSERT INTO append_payloads(token, message, flags, sha256) "
232 "VALUES(:token, :message, :flags, :sha)"));
233
2/2
✓ Branch 110 → 111 taken 16 times.
✓ Branch 111 → 112 taken 16 times.
16 insert.bindValue(":token", tokens->first());
234
2/2
✓ Branch 116 → 117 taken 16 times.
✓ Branch 117 → 118 taken 16 times.
16 insert.bindValue(":message", message);
235
6/8
✓ Branch 122 → 123 taken 2 times.
✓ Branch 122 → 126 taken 14 times.
✓ Branch 128 → 129 taken 16 times.
✓ Branch 129 → 130 taken 16 times.
✓ Branch 133 → 134 taken 2 times.
✓ Branch 133 → 135 taken 14 times.
✗ Branch 239 → 240 not taken.
✗ Branch 239 → 241 not taken.
18 insert.bindValue(":flags", flags.isNull() ? QStringLiteral("") : flags);
236
2/2
✓ Branch 142 → 143 taken 16 times.
✓ Branch 143 → 144 taken 16 times.
32 insert.bindValue(":sha", precomputedSha256.isEmpty()
237
3/3
✓ Branch 137 → 138 taken 11 times.
✓ Branch 137 → 140 taken 5 times.
✓ Branch 139 → 141 taken 11 times.
32 ? QCryptographicHash::hash(message, QCryptographicHash::Sha256)
238 : precomputedSha256);
239
3/3
✓ Branch 147 → 148 taken 16 times.
✓ Branch 148 → 149 taken 1 time.
✓ Branch 148 → 157 taken 15 times.
16 if (!insert.exec()) {
240
2/2
✓ Branch 149 → 150 taken 1 time.
✓ Branch 150 → 151 taken 1 time.
1 m_lastError = insert.lastError().text();
241 1 return std::nullopt;
242 }
243
2/3
✓ Branch 157 → 158 taken 15 times.
✗ Branch 158 → 159 not taken.
✓ Branch 158 → 167 taken 15 times.
15 if (!m_db.commit()) {
244 ✗ m_lastError = m_db.lastError().text();
245 ✗ return std::nullopt;
246 }
247 15 rollback.dismiss();
248 15 return tokens->first();
249 35 }
250
251 25 std::optional<MutationJournalStore::AppendPayload> MutationJournalStore::appendPayload(qint64 token,
252 const QString &accountId, QString *readError) const {
253
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 4 taken 20 times.
25 if (readError) readError->clear();
254
1/1
✓ Branch 4 → 5 taken 25 times.
25 QSqlQuery query(m_db);
255
1/1
✓ Branch 8 → 9 taken 25 times.
25 query.prepare(QStringLiteral("SELECT p.message, p.flags, p.sha256 FROM append_payloads p "
256 "JOIN mutation_journal j ON j.token = p.token "
257 "WHERE p.token = :token AND j.account_id = :account AND j.kind = :kind "
258 "AND length(p.message) > 0 AND length(p.message) <= :limit AND length(p.flags) <= 256"));
259
2/2
✓ Branch 13 → 14 taken 25 times.
✓ Branch 14 → 15 taken 25 times.
25 query.bindValue(":token", token);
260
2/2
✓ Branch 19 → 20 taken 25 times.
✓ Branch 20 → 21 taken 25 times.
25 query.bindValue(":account", accountId);
261
2/2
✓ Branch 25 → 26 taken 25 times.
✓ Branch 26 → 27 taken 25 times.
25 query.bindValue(":kind", static_cast<int>(MutationKind::Append));
262
2/2
✓ Branch 31 → 32 taken 25 times.
✓ Branch 32 → 33 taken 25 times.
25 query.bindValue(":limit", MaximumAppendMessageBytes);
263
7/8
✓ Branch 35 → 36 taken 25 times.
✓ Branch 36 → 37 taken 25 times.
✗ Branch 36 → 39 not taken.
✓ Branch 37 → 38 taken 25 times.
✓ Branch 38 → 39 taken 7 times.
✓ Branch 38 → 40 taken 18 times.
✓ Branch 41 → 42 taken 7 times.
✓ Branch 41 → 58 taken 18 times.
25 if (!query.exec() || !query.next()) {
264
8/12
✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 47 taken 5 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 44 → 45 taken 2 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 2 times.
✓ Branch 48 → 49 taken 2 times.
✓ Branch 48 → 50 taken 5 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 57 taken 7 times.
✗ Branch 130 → 131 not taken.
✗ Branch 130 → 132 not taken.
7 if (readError && query.lastError().isValid())
265 ✗ *readError = query.lastError().text();
266 7 return std::nullopt;
267 }
268
4/6
✓ Branch 58 → 59 taken 18 times.
✓ Branch 59 → 60 taken 18 times.
✓ Branch 60 → 61 taken 18 times.
✓ Branch 61 → 62 taken 18 times.
✗ Branch 141 → 142 not taken.
✗ Branch 141 → 143 not taken.
18 AppendPayload payload{query.value(0).toByteArray(), query.value(1).toString()};
269
6/14
✓ Branch 65 → 66 taken 18 times.
✓ Branch 67 → 68 taken 18 times.
✓ Branch 85 → 86 taken 18 times.
✗ Branch 85 → 87 not taken.
✓ Branch 87 → 88 taken 18 times.
✗ Branch 87 → 89 not taken.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 91 taken 17 times.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
✗ Branch 157 → 158 not taken.
✗ Branch 157 → 159 not taken.
✗ Branch 161 → 162 not taken.
✗ Branch 161 → 163 not taken.
54 if (QCryptographicHash::hash(payload.message, QCryptographicHash::Sha256) != query.value(2).toByteArray() ||
270
7/9
✓ Branch 64 → 65 taken 18 times.
✓ Branch 69 → 70 taken 17 times.
✓ Branch 69 → 81 taken 1 time.
✓ Branch 72 → 73 taken 17 times.
✓ Branch 73 → 74 taken 17 times.
✗ Branch 73 → 81 not taken.
✓ Branch 76 → 77 taken 17 times.
✓ Branch 77 → 78 taken 17 times.
✗ Branch 77 → 81 not taken.
71 payload.flags.contains(QLatin1Char('\r')) || payload.flags.contains(QLatin1Char('\n')) ||
271
3/5
✓ Branch 79 → 80 taken 17 times.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 82 taken 17 times.
✓ Branch 83 → 84 taken 18 times.
✗ Branch 83 → 85 not taken.
36 payload.flags.contains(QChar::Null)) return std::nullopt;
272 17 return payload;
273 25 }
274
275 6 qint64 MutationJournalStore::appendPayloadBytes() const {
276
1/1
✓ Branch 2 → 3 taken 6 times.
6 QSqlQuery query(m_db);
277
5/13
✓ Branch 6 → 7 taken 6 times.
✓ Branch 7 → 8 taken 6 times.
✗ Branch 7 → 10 not taken.
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 6 times.
✗ Branch 14 → 16 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 6 times.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
18 if (!query.exec(QStringLiteral("SELECT COALESCE(SUM(length(message)), 0) FROM append_payloads")) ||
278
2/3
✓ Branch 8 → 9 taken 6 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
6 !query.next()) return -1;
279
2/2
✓ Branch 18 → 19 taken 6 times.
✓ Branch 19 → 20 taken 6 times.
6 return query.value(0).toLongLong();
280 6 }
281
282 std::optional<QList<qint64>>
283 21 MutationJournalStore::enqueueMutations(const QList<MutationRecord> &records) {
284
3/3
✓ Branch 2 → 3 taken 21 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 11 taken 20 times.
21 if (!m_db.isOpen()) {
285 1 m_lastError = QStringLiteral("Mutation journal database is not open");
286 1 return std::nullopt;
287 }
288
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 17 taken 19 times.
20 if (records.isEmpty()) return QList<qint64>{};
289
3/3
✓ Branch 17 → 18 taken 19 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 18 times.
19 if (!beginWriteTransaction(m_db, &m_lastError)) return std::nullopt;
290
1/1
✓ Branch 20 → 21 taken 18 times.
18 const auto tokens = enqueueMutationsInTransaction(records);
291
2/2
✓ Branch 22 → 23 taken 14 times.
✓ Branch 22 → 25 taken 4 times.
18 if (!tokens) {
292
1/1
✓ Branch 23 → 24 taken 14 times.
14 m_db.rollback();
293 14 return std::nullopt;
294 }
295
2/3
✓ Branch 25 → 26 taken 4 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 34 taken 4 times.
4 if (!m_db.commit()) {
296 ✗ m_lastError = m_db.lastError().text();
297 ✗ m_db.rollback();
298 ✗ return std::nullopt;
299 }
300 // A newly journaled Move is the first pending phase. Indexed thread pages
301 // hide the row from this signal; they must not depend on the (possibly
302 // unloaded) flat model mirroring the optimistic removal.
303
5/6
✓ Branch 42 → 43 taken 7 times.
✓ Branch 42 → 47 taken 4 times.
✓ Branch 45 → 46 taken 7 times.
✗ Branch 45 → 47 not taken.
✓ Branch 48 → 35 taken 7 times.
✓ Branch 48 → 49 taken 4 times.
11 for (int i = 0; i < records.size() && i < tokens->size(); ++i) {
304 7 const auto &record = records.at(i);
305
2/2
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 40 taken 5 times.
7 if (record.kind == MutationKind::Move)
306
1/1
✓ Branch 39 → 40 taken 2 times.
2 m_notify.mutationPhaseChanged(record.mailId.accountId, record.mailId.folderId,
307 2 tokens->at(i), record.mailId.uid, true);
308 }
309
1/1
✓ Branch 49 → 50 taken 4 times.
4 return tokens;
310 18 }
311
312 181 std::optional<QList<qint64>> MutationJournalStore::enqueueMutationsInTransaction(
313 const QList<MutationRecord> &records, bool *capacityExceeded) {
314
2/2
✓ Branch 2 → 3 taken 91 times.
✓ Branch 2 → 4 taken 90 times.
181 if (capacityExceeded) *capacityExceeded = false;
315 6101 const auto validRecord = [](const MutationRecord &record) {
316
4/4
✓ Branch 5 → 6 taken 6099 times.
✓ Branch 5 → 19 taken 1 time.
✓ Branch 7 → 8 taken 6098 times.
✓ Branch 7 → 19 taken 1 time.
18300 return record.isValid() && record.mailId.accountId.size() <= 256 &&
317
4/4
✓ Branch 9 → 10 taken 6097 times.
✓ Branch 9 → 19 taken 1 time.
✓ Branch 11 → 12 taken 6096 times.
✓ Branch 11 → 19 taken 1 time.
18294 record.folderPath.size() <= 1024 && record.flag.size() <= 256 &&
318
2/2
✓ Branch 13 → 14 taken 6095 times.
✓ Branch 13 → 19 taken 1 time.
12193 record.targetFolder.size() <= 1024 &&
319
2/2
✓ Branch 15 → 16 taken 6094 times.
✓ Branch 15 → 19 taken 1 time.
12191 record.messageId.size() <= 2048 &&
320
4/4
✓ Branch 3 → 4 taken 6100 times.
✓ Branch 3 → 19 taken 1 time.
✓ Branch 17 → 18 taken 6093 times.
✓ Branch 17 → 19 taken 1 time.
18296 record.uidPlusEvidence.size() <= 1024 && record.error.size() <= 2048;
321 };
322
4/5
✓ Branch 4 → 5 taken 181 times.
✓ Branch 5 → 6 taken 181 times.
✗ Branch 5 → 10 not taken.
✓ Branch 12 → 13 taken 8 times.
✓ Branch 12 → 20 taken 173 times.
362 if (!m_db.isOpen() ||
323
3/3
✓ Branch 8 → 9 taken 181 times.
✓ Branch 9 → 10 taken 8 times.
✓ Branch 9 → 11 taken 173 times.
181 std::any_of(records.cbegin(), records.cend(),
324 6101 [&validRecord](const MutationRecord &record) {
325 6101 return !validRecord(record);
326 })) {
327 8 m_lastError = QStringLiteral("Invalid or oversized mutation journal entry");
328 8 return std::nullopt;
329 }
330
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 26 taken 173 times.
173 if (records.isEmpty())
331 ✗ return QList<qint64>{};
332 9 const auto fail = [this, capacityExceeded](const QString &error, bool limited = false)
333 -> std::optional<QList<qint64>> {
334 9 m_lastError = error;
335
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 7 times.
9 if (capacityExceeded) *capacityExceeded = limited;
336 9 return std::nullopt;
337 173 };
338
1/1
✓ Branch 26 → 27 taken 173 times.
173 QSqlQuery capacity(m_db);
339
3/8
✓ Branch 38 → 39 taken 173 times.
✗ Branch 38 → 40 not taken.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 47 taken 171 times.
✗ Branch 419 → 420 not taken.
✗ Branch 419 → 421 not taken.
✗ Branch 423 → 424 not taken.
✗ Branch 423 → 425 not taken.
346 if (!capacity.exec(QStringLiteral(
340 "SELECT COUNT(*), COALESCE(SUM(128 + 2 * (length(account_id) + "
341 "length(folder_path) + length(flag_name) + length(target_folder) + "
342 "length(message_id) + length(uidplus_evidence) + length(error))), 0) "
343
4/5
✓ Branch 30 → 31 taken 173 times.
✓ Branch 31 → 32 taken 171 times.
✓ Branch 31 → 34 taken 2 times.
✓ Branch 36 → 37 taken 173 times.
✗ Branch 36 → 38 not taken.
517 "FROM mutation_journal")) ||
344
2/3
✓ Branch 32 → 33 taken 171 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 171 times.
171 !capacity.next()) {
345
2/2
✓ Branch 41 → 42 taken 2 times.
✓ Branch 42 → 43 taken 2 times.
2 return fail(capacity.lastError().text());
346 }
347
2/2
✓ Branch 47 → 48 taken 171 times.
✓ Branch 48 → 49 taken 171 times.
171 int currentCount = capacity.value(0).toInt();
348
2/2
✓ Branch 50 → 51 taken 171 times.
✓ Branch 51 → 52 taken 171 times.
171 qint64 currentBytes = capacity.value(1).toLongLong();
349
350
1/1
✓ Branch 53 → 54 taken 171 times.
171 QSqlQuery existing(m_db);
351
1/1
✓ Branch 57 → 58 taken 171 times.
171 existing.prepare(QStringLiteral(
352 "SELECT token, 128 + 2 * (length(account_id) + length(folder_path) + "
353 "length(flag_name) + length(target_folder) + length(message_id) + "
354 "length(uidplus_evidence) + length(error)) "
355 "FROM mutation_journal WHERE account_id = :account "
356 "AND folder_id = :folder AND uid = :uid AND kind = :kind "
357 "AND flag_name = :flag AND replay_class = :class "
358 "AND phase = :queued ORDER BY token DESC LIMIT 1"));
359
1/1
✓ Branch 60 → 61 taken 171 times.
171 QSqlQuery update(m_db);
360
1/1
✓ Branch 64 → 65 taken 171 times.
171 update.prepare(QStringLiteral(
361 "UPDATE mutation_journal SET desired_state = :desired, "
362 "folder_path = :path, uidvalidity = :validity, phase = :phase, "
363 "error = '', uidplus_evidence = '', updated_at = :updated "
364 "WHERE token = :token"));
365
1/1
✓ Branch 67 → 68 taken 171 times.
171 QSqlQuery insert(m_db);
366
1/1
✓ Branch 71 → 72 taken 171 times.
171 insert.prepare(QStringLiteral(
367 "INSERT INTO mutation_journal (account_id, folder_id, folder_path, uid, "
368 "kind, replay_class, phase, flag_name, desired_state, target_folder, "
369 "message_id, uidvalidity, uidplus_evidence, error, created_at, updated_at) "
370 "VALUES (:account, :folder, :path, :uid, :kind, :class, :phase, :flag, "
371 ":desired, :target, :message, :validity, :uidplus, :error, :created, "
372 ":updated)"));
373
374 171 const qint64 now = QDateTime::currentSecsSinceEpoch();
375 171 QList<qint64> tokens;
376
1/1
✓ Branch 76 → 77 taken 171 times.
171 tokens.reserve(records.size());
377
2/2
✓ Branch 409 → 79 taken 6091 times.
✓ Branch 409 → 410 taken 164 times.
6255 for (const MutationRecord &record : records) {
378
2/2
✓ Branch 80 → 81 taken 311 times.
✓ Branch 80 → 233 taken 5780 times.
6091 if (record.replayClass == MutationReplayClass::DesiredState) {
379
1/1
✓ Branch 86 → 87 taken 311 times.
622 existing.bindValue(QStringLiteral(":account"), record.mailId.accountId);
380
1/1
✓ Branch 95 → 96 taken 311 times.
622 existing.bindValue(QStringLiteral(":folder"), record.mailId.folderId);
381
1/1
✓ Branch 104 → 105 taken 311 times.
622 existing.bindValue(QStringLiteral(":uid"), record.mailId.uid);
382
1/1
✓ Branch 113 → 114 taken 311 times.
622 existing.bindValue(QStringLiteral(":kind"), static_cast<int>(record.kind));
383
1/1
✓ Branch 122 → 123 taken 311 times.
622 existing.bindValue(QStringLiteral(":flag"), record.flag);
384
1/1
✓ Branch 131 → 132 taken 311 times.
622 existing.bindValue(QStringLiteral(":class"),
385 static_cast<int>(MutationReplayClass::DesiredState));
386
1/1
✓ Branch 140 → 141 taken 311 times.
622 existing.bindValue(QStringLiteral(":queued"),
387 static_cast<int>(MutationPhase::Queued));
388
2/3
✓ Branch 144 → 145 taken 311 times.
✗ Branch 145 → 146 not taken.
✓ Branch 145 → 152 taken 311 times.
311 if (!existing.exec())
389 ✗ return fail(existing.lastError().text());
390
3/3
✓ Branch 152 → 153 taken 311 times.
✓ Branch 153 → 154 taken 13 times.
✓ Branch 153 → 233 taken 298 times.
311 if (existing.next()) {
391
2/2
✓ Branch 154 → 155 taken 13 times.
✓ Branch 155 → 156 taken 13 times.
13 const qint64 token = existing.value(0).toLongLong();
392 13 currentBytes += estimatedMutationBytes(record) -
393
2/2
✓ Branch 158 → 159 taken 13 times.
✓ Branch 159 → 160 taken 13 times.
13 existing.value(1).toLongLong();
394
2/2
✓ Branch 161 → 162 taken 1 time.
✓ Branch 161 → 169 taken 12 times.
13 if (currentBytes > m_mutationJournalMaxBytes)
395 1 return fail(QStringLiteral("Mutation journal capacity exceeded"), true);
396
1/1
✓ Branch 174 → 175 taken 12 times.
24 update.bindValue(QStringLiteral(":desired"), record.desiredState);
397
1/1
✓ Branch 183 → 184 taken 12 times.
24 update.bindValue(QStringLiteral(":path"), record.folderPath);
398
1/1
✓ Branch 192 → 193 taken 12 times.
24 update.bindValue(QStringLiteral(":validity"), record.uidValidity);
399
1/1
✓ Branch 201 → 202 taken 12 times.
24 update.bindValue(QStringLiteral(":phase"),
400 static_cast<int>(MutationPhase::Queued));
401
1/1
✓ Branch 210 → 211 taken 12 times.
24 update.bindValue(QStringLiteral(":updated"), now);
402
1/1
✓ Branch 219 → 220 taken 12 times.
24 update.bindValue(QStringLiteral(":token"), token);
403
3/3
✓ Branch 223 → 224 taken 12 times.
✓ Branch 224 → 225 taken 1 time.
✓ Branch 224 → 231 taken 11 times.
12 if (!update.exec())
404
2/2
✓ Branch 225 → 226 taken 1 time.
✓ Branch 226 → 227 taken 1 time.
1 return fail(update.lastError().text());
405
1/1
✓ Branch 231 → 232 taken 11 times.
11 tokens.append(token);
406 11 continue;
407 11 }
408 }
409
410 6078 ++currentCount;
411 6078 currentBytes += estimatedMutationBytes(record);
412
2/2
✓ Branch 234 → 235 taken 6075 times.
✓ Branch 234 → 236 taken 3 times.
6078 if (currentCount > m_mutationJournalMaxEntries ||
413
1/2
✗ Branch 235 → 236 not taken.
✓ Branch 235 → 243 taken 6075 times.
6075 currentBytes > m_mutationJournalMaxBytes)
414 3 return fail(QStringLiteral("Mutation journal capacity exceeded"), true);
415
1/1
✓ Branch 248 → 249 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":account"), record.mailId.accountId);
416
1/1
✓ Branch 257 → 258 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":folder"), record.mailId.folderId);
417
1/1
✓ Branch 266 → 267 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":path"), record.folderPath);
418
1/1
✓ Branch 275 → 276 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":uid"), record.mailId.uid);
419
1/1
✓ Branch 284 → 285 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":kind"), static_cast<int>(record.kind));
420
1/1
✓ Branch 293 → 294 taken 6075 times.
6075 insert.bindValue(QStringLiteral(":class"),
421 6075 static_cast<int>(record.replayClass));
422
1/1
✓ Branch 302 → 303 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":phase"), static_cast<int>(record.phase));
423
1/1
✓ Branch 311 → 312 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":flag"), record.flag);
424
1/1
✓ Branch 320 → 321 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":desired"), record.desiredState);
425
1/1
✓ Branch 329 → 330 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":target"), record.targetFolder);
426
1/1
✓ Branch 338 → 339 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":message"), record.messageId);
427
1/1
✓ Branch 347 → 348 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":validity"), record.uidValidity);
428
1/1
✓ Branch 356 → 357 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":uidplus"), record.uidPlusEvidence);
429
1/1
✓ Branch 365 → 366 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":error"), record.error);
430
1/1
✓ Branch 374 → 375 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":created"), now);
431
1/1
✓ Branch 383 → 384 taken 6075 times.
12150 insert.bindValue(QStringLiteral(":updated"), now);
432
3/3
✓ Branch 387 → 388 taken 6075 times.
✓ Branch 388 → 389 taken 2 times.
✓ Branch 388 → 395 taken 6073 times.
6075 if (!insert.exec())
433
2/2
✓ Branch 389 → 390 taken 2 times.
✓ Branch 390 → 391 taken 2 times.
2 return fail(insert.lastError().text());
434
2/2
✓ Branch 395 → 396 taken 6073 times.
✓ Branch 396 → 397 taken 6073 times.
6073 const qint64 token = insert.lastInsertId().toLongLong();
435
1/2
✗ Branch 398 → 399 not taken.
✓ Branch 398 → 406 taken 6073 times.
6073 if (token <= 0)
436 ✗ return fail(QStringLiteral("Mutation journal did not return a token"));
437
1/1
✓ Branch 406 → 407 taken 6073 times.
6073 tokens.append(token);
438 }
439 164 return tokens;
440 173 }
441
442 QList<MutationRecord>
443 4057 MutationJournalStore::pendingMutations(const QString &accountId) const {
444 4057 QList<MutationRecord> records;
445
1/1
✓ Branch 2 → 3 taken 4057 times.
4057 QSqlQuery query(m_db);
446 8114 QString sql = QStringLiteral("SELECT %1 FROM mutation_journal ").arg(
447
2/2
✓ Branch 8 → 9 taken 4057 times.
✓ Branch 9 → 10 taken 4057 times.
4057 QString::fromLatin1(MutationColumns));
448
2/2
✓ Branch 14 → 15 taken 4052 times.
✓ Branch 14 → 22 taken 5 times.
4057 if (!accountId.isEmpty())
449
1/1
✓ Branch 18 → 19 taken 4052 times.
4052 sql += QStringLiteral("WHERE account_id = :account ");
450
1/1
✓ Branch 25 → 26 taken 4057 times.
4057 sql += QStringLiteral("ORDER BY token");
451
1/1
✓ Branch 28 → 29 taken 4057 times.
4057 query.prepare(sql);
452
2/2
✓ Branch 30 → 31 taken 4052 times.
✓ Branch 30 → 41 taken 5 times.
4057 if (!accountId.isEmpty())
453
1/1
✓ Branch 36 → 37 taken 4052 times.
8104 query.bindValue(QStringLiteral(":account"), accountId);
454
3/3
✓ Branch 41 → 42 taken 4057 times.
✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 44 taken 4055 times.
4057 if (!query.exec())
455 2 return records;
456
3/3
✓ Branch 49 → 50 taken 2947624 times.
✓ Branch 50 → 45 taken 2943569 times.
✓ Branch 50 → 51 taken 4055 times.
2947624 while (query.next())
457
2/2
✓ Branch 45 → 46 taken 2943569 times.
✓ Branch 46 → 47 taken 2943569 times.
2943569 records.append(mutationFromQuery(query));
458 4055 return records;
459 4057 }
460
461 23 std::optional<QList<MutationRecord>> MutationJournalStore::pendingAppendMutations(const QString &accountId, int limit) const {
462
4/8
✓ Branch 3 → 4 taken 23 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 23 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 23 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 23 times.
23 if (accountId.isEmpty() || limit < 1 || limit > 100) return std::nullopt;
463
1/1
✓ Branch 10 → 11 taken 23 times.
23 QSqlQuery query(m_db);
464
1/1
✓ Branch 18 → 19 taken 23 times.
69 query.prepare(QStringLiteral("SELECT %1 FROM mutation_journal WHERE account_id = :account "
465 "AND kind = :kind AND phase != :confirmed ORDER BY token LIMIT :limit")
466
2/2
✓ Branch 16 → 17 taken 23 times.
✓ Branch 17 → 18 taken 23 times.
69 .arg(QString::fromLatin1(MutationColumns)));
467
2/2
✓ Branch 25 → 26 taken 23 times.
✓ Branch 26 → 27 taken 23 times.
23 query.bindValue(":account", accountId);
468
2/2
✓ Branch 31 → 32 taken 23 times.
✓ Branch 32 → 33 taken 23 times.
23 query.bindValue(":kind", static_cast<int>(MutationKind::Append));
469
2/2
✓ Branch 37 → 38 taken 23 times.
✓ Branch 38 → 39 taken 23 times.
23 query.bindValue(":confirmed", static_cast<int>(MutationPhase::Confirmed));
470
2/2
✓ Branch 43 → 44 taken 23 times.
✓ Branch 44 → 45 taken 23 times.
23 query.bindValue(":limit", limit);
471
2/3
✓ Branch 47 → 48 taken 23 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 23 times.
23 if (!query.exec()) return std::nullopt;
472 23 QList<MutationRecord> rows;
473
5/5
✓ Branch 51 → 52 taken 27 times.
✓ Branch 52 → 53 taken 27 times.
✓ Branch 55 → 56 taken 50 times.
✓ Branch 56 → 51 taken 27 times.
✓ Branch 56 → 57 taken 23 times.
50 while (query.next()) rows.append(mutationFromQuery(query));
474 23 return rows;
475 23 }
476
477 1 bool MutationJournalStore::requeueFailedAppend(qint64 token, const QString &accountId) {
478
1/1
✓ Branch 2 → 3 taken 1 time.
1 QSqlQuery query(m_db);
479
1/1
✓ Branch 6 → 7 taken 1 time.
1 query.prepare(QStringLiteral("UPDATE mutation_journal SET phase = :queued, error = '', updated_at = :now "
480 "WHERE token = :token AND account_id = :account AND kind = :kind AND phase = :failed "
481 "AND EXISTS (SELECT 1 FROM append_payloads p WHERE p.token = :token "
482 "AND length(p.message) > 0 AND length(p.message) <= :limit "
483 "AND length(p.sha256) = 32 AND length(p.flags) <= 256)"));
484
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
1 query.bindValue(":queued", static_cast<int>(MutationPhase::Queued));
485
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
1 query.bindValue(":now", QDateTime::currentSecsSinceEpoch());
486
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
1 query.bindValue(":token", token);
487
2/2
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
1 query.bindValue(":account", accountId);
488
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
1 query.bindValue(":kind", static_cast<int>(MutationKind::Append));
489
2/2
✓ Branch 42 → 43 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
1 query.bindValue(":failed", static_cast<int>(MutationPhase::Failed));
490
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
1 query.bindValue(":limit", MaximumAppendMessageBytes);
491
4/6
✓ Branch 52 → 53 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
✗ Branch 53 → 57 not taken.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
✗ Branch 55 → 57 not taken.
2 return query.exec() && query.numRowsAffected() == 1;
492 1 }
493
494 5074 std::optional<MutationRecord> MutationJournalStore::mutation(qint64 token) const {
495
1/1
✓ Branch 2 → 3 taken 5074 times.
5074 QSqlQuery query(m_db);
496
1/1
✓ Branch 10 → 11 taken 5074 times.
15222 query.prepare(QStringLiteral("SELECT %1 FROM mutation_journal WHERE token = "
497 ":token")
498
2/2
✓ Branch 8 → 9 taken 5074 times.
✓ Branch 9 → 10 taken 5074 times.
15222 .arg(QString::fromLatin1(MutationColumns)));
499
1/1
✓ Branch 20 → 21 taken 5074 times.
10148 query.bindValue(QStringLiteral(":token"), token);
500
8/8
✓ Branch 24 → 25 taken 5074 times.
✓ Branch 25 → 26 taken 5072 times.
✓ Branch 25 → 28 taken 2 times.
✓ Branch 26 → 27 taken 5072 times.
✓ Branch 27 → 28 taken 54 times.
✓ Branch 27 → 29 taken 5018 times.
✓ Branch 30 → 31 taken 56 times.
✓ Branch 30 → 32 taken 5018 times.
5074 if (!query.exec() || !query.next())
501 56 return std::nullopt;
502
1/1
✓ Branch 32 → 33 taken 5018 times.
5018 return mutationFromQuery(query);
503 5074 }
504
505 63 bool MutationJournalStore::claimMutationForDispatch(const MutationRecord &expected) {
506
6/9
✓ Branch 2 → 3 taken 63 times.
✓ Branch 3 → 4 taken 63 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 63 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 63 times.
✗ Branch 6 → 8 not taken.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 62 times.
126 if (!m_db.isOpen() || expected.token <= 0 || !expected.isValid() ||
507
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 62 times.
63 expected.phase != MutationPhase::Queued)
508 1 return false;
509 // One SQLite write statement arbitrates with worker-owned cancellation.
510 // Compare execution fields, not timestamps (which have second resolution).
511
1/1
✓ Branch 12 → 13 taken 62 times.
62 QSqlQuery query(m_db);
512
1/1
✓ Branch 16 → 17 taken 62 times.
62 query.prepare(QStringLiteral(
513 "UPDATE mutation_journal SET phase=:dispatched, updated_at=:now "
514 "WHERE token=:token AND phase=:queued AND account_id=:account "
515 "AND folder_id=:folder AND folder_path=:path AND uid=:uid "
516 "AND kind=:kind AND replay_class=:class AND uidvalidity=:validity "
517 "AND COALESCE(flag_name,'')=:flag AND desired_state=:desired "
518 "AND COALESCE(target_folder,'')=:target AND COALESCE(message_id,'')=:message "
519 "AND (:append OR EXISTS (SELECT 1 FROM folders f WHERE f.id=:folder "
520 "AND f.account=:account AND f.path=:path "
521 "AND (:validity=0 OR f.uidvalidity=0 OR f.uidvalidity=:validity)))"));
522
2/2
✓ Branch 21 → 22 taken 62 times.
✓ Branch 22 → 23 taken 62 times.
62 query.bindValue(":dispatched", static_cast<int>(MutationPhase::Dispatched));
523
2/2
✓ Branch 28 → 29 taken 62 times.
✓ Branch 29 → 30 taken 62 times.
62 query.bindValue(":now", QDateTime::currentSecsSinceEpoch());
524
2/2
✓ Branch 34 → 35 taken 62 times.
✓ Branch 35 → 36 taken 62 times.
62 query.bindValue(":token", expected.token);
525
2/2
✓ Branch 40 → 41 taken 62 times.
✓ Branch 41 → 42 taken 62 times.
62 query.bindValue(":queued", static_cast<int>(MutationPhase::Queued));
526
2/2
✓ Branch 46 → 47 taken 62 times.
✓ Branch 47 → 48 taken 62 times.
62 query.bindValue(":account", expected.mailId.accountId);
527
2/2
✓ Branch 52 → 53 taken 62 times.
✓ Branch 53 → 54 taken 62 times.
62 query.bindValue(":folder", expected.mailId.folderId);
528
2/2
✓ Branch 58 → 59 taken 62 times.
✓ Branch 59 → 60 taken 62 times.
62 query.bindValue(":path", expected.folderPath);
529
2/2
✓ Branch 64 → 65 taken 62 times.
✓ Branch 65 → 66 taken 62 times.
62 query.bindValue(":uid", expected.mailId.uid);
530
2/2
✓ Branch 70 → 71 taken 62 times.
✓ Branch 71 → 72 taken 62 times.
62 query.bindValue(":kind", static_cast<int>(expected.kind));
531
2/2
✓ Branch 76 → 77 taken 62 times.
✓ Branch 77 → 78 taken 62 times.
62 query.bindValue(":class", static_cast<int>(expected.replayClass));
532
2/2
✓ Branch 82 → 83 taken 62 times.
✓ Branch 83 → 84 taken 62 times.
62 query.bindValue(":validity", expected.uidValidity);
533
6/8
✓ Branch 88 → 89 taken 51 times.
✓ Branch 88 → 92 taken 11 times.
✓ Branch 94 → 95 taken 62 times.
✓ Branch 95 → 96 taken 62 times.
✓ Branch 99 → 100 taken 51 times.
✓ Branch 99 → 101 taken 11 times.
✗ Branch 246 → 247 not taken.
✗ Branch 246 → 248 not taken.
113 query.bindValue(":flag", expected.flag.isNull() ? QStringLiteral("") : expected.flag);
534
2/2
✓ Branch 103 → 104 taken 62 times.
✓ Branch 104 → 105 taken 62 times.
62 query.bindValue(":desired", expected.desiredState);
535
6/8
✓ Branch 109 → 110 taken 2 times.
✓ Branch 109 → 113 taken 60 times.
✓ Branch 115 → 116 taken 62 times.
✓ Branch 116 → 117 taken 62 times.
✓ Branch 120 → 121 taken 2 times.
✓ Branch 120 → 122 taken 60 times.
✗ Branch 267 → 268 not taken.
✗ Branch 267 → 269 not taken.
64 query.bindValue(":target", expected.targetFolder.isNull() ? QStringLiteral("") : expected.targetFolder);
536
6/8
✓ Branch 124 → 125 taken 2 times.
✓ Branch 124 → 128 taken 60 times.
✓ Branch 130 → 131 taken 62 times.
✓ Branch 131 → 132 taken 62 times.
✓ Branch 135 → 136 taken 2 times.
✓ Branch 135 → 137 taken 60 times.
✗ Branch 281 → 282 not taken.
✗ Branch 281 → 283 not taken.
64 query.bindValue(":message", expected.messageId.isNull() ? QStringLiteral("") : expected.messageId);
537
2/2
✓ Branch 139 → 140 taken 62 times.
✓ Branch 140 → 141 taken 62 times.
62 query.bindValue(":append", expected.kind == MutationKind::Append);
538
2/3
✓ Branch 143 → 144 taken 62 times.
✗ Branch 144 → 145 not taken.
✓ Branch 144 → 151 taken 62 times.
62 if (!query.exec()) {
539 ✗ m_lastError = query.lastError().text();
540 ✗ return false;
541 }
542
1/1
✓ Branch 151 → 152 taken 62 times.
62 return query.numRowsAffected() == 1;
543 62 }
544
545 109 bool MutationJournalStore::updateMutationPhase(qint64 token, MutationPhase phase,
546 const QString &error,
547 const QString &uidPlusEvidence) {
548
6/6
✓ Branch 3 → 4 taken 108 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 107 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 107 times.
109 if (error.size() > 2048 || uidPlusEvidence.size() > 1024)
549 2 return false;
550 // P90.16: the phase drives pending-move admission sets; owners need the
551 // transition to refresh their projection exclusions even when the folder
552 // revision is unchanged.
553
1/1
✓ Branch 10 → 11 taken 107 times.
107 QSqlQuery owner(m_db);
554
1/1
✓ Branch 14 → 15 taken 107 times.
107 owner.prepare(QStringLiteral("SELECT account_id, folder_id, kind, uid FROM mutation_journal WHERE token=:token"));
555
1/1
✓ Branch 22 → 23 taken 107 times.
214 owner.bindValue(QStringLiteral(":token"), token);
556
6/6
✓ Branch 26 → 27 taken 107 times.
✓ Branch 27 → 28 taken 105 times.
✓ Branch 27 → 31 taken 2 times.
✓ Branch 28 → 29 taken 105 times.
✓ Branch 29 → 30 taken 104 times.
✓ Branch 29 → 31 taken 1 time.
107 const bool hasOwner = owner.exec() && owner.next();
557
6/8
✓ Branch 32 → 33 taken 104 times.
✓ Branch 32 → 35 taken 3 times.
✓ Branch 33 → 34 taken 104 times.
✓ Branch 34 → 36 taken 104 times.
✓ Branch 36 → 37 taken 104 times.
✓ Branch 36 → 38 taken 3 times.
✗ Branch 150 → 151 not taken.
✗ Branch 150 → 152 not taken.
107 const QString account = hasOwner ? owner.value(0).toString() : QString{};
558
6/8
✓ Branch 38 → 39 taken 104 times.
✓ Branch 38 → 42 taken 3 times.
✓ Branch 39 → 40 taken 104 times.
✓ Branch 40 → 41 taken 104 times.
✓ Branch 43 → 44 taken 104 times.
✓ Branch 43 → 45 taken 3 times.
✗ Branch 154 → 155 not taken.
✗ Branch 154 → 156 not taken.
107 const qint64 folderId = hasOwner ? owner.value(1).toLongLong() : qint64(-1);
559
2/2
✓ Branch 45 → 46 taken 104 times.
✓ Branch 45 → 50 taken 3 times.
211 const bool mayChangeMoveExclusions = hasOwner &&
560
6/8
✓ Branch 46 → 47 taken 104 times.
✓ Branch 47 → 48 taken 104 times.
✓ Branch 48 → 49 taken 40 times.
✓ Branch 48 → 50 taken 64 times.
✓ Branch 51 → 52 taken 104 times.
✓ Branch 51 → 53 taken 3 times.
✗ Branch 158 → 159 not taken.
✗ Branch 158 → 160 not taken.
211 owner.value(2).toInt() == int(MutationKind::Move);
561
6/8
✓ Branch 53 → 54 taken 40 times.
✓ Branch 53 → 57 taken 67 times.
✓ Branch 54 → 55 taken 40 times.
✓ Branch 55 → 56 taken 40 times.
✓ Branch 58 → 59 taken 40 times.
✓ Branch 58 → 60 taken 67 times.
✗ Branch 162 → 163 not taken.
✗ Branch 162 → 164 not taken.
107 const qint64 uid = mayChangeMoveExclusions ? owner.value(3).toLongLong() : qint64(0);
562
1/1
✓ Branch 60 → 61 taken 107 times.
107 owner.finish();
563
1/1
✓ Branch 61 → 62 taken 107 times.
107 QSqlQuery query(m_db);
564
1/1
✓ Branch 65 → 66 taken 107 times.
107 query.prepare(QStringLiteral(
565 "UPDATE mutation_journal SET phase = :phase, error = :error, "
566 "uidplus_evidence = :uidplus, updated_at = :updated WHERE token = "
567 ":token"));
568
1/1
✓ Branch 73 → 74 taken 107 times.
214 query.bindValue(QStringLiteral(":phase"), static_cast<int>(phase));
569
1/1
✓ Branch 82 → 83 taken 107 times.
214 query.bindValue(QStringLiteral(":error"), error);
570
1/1
✓ Branch 91 → 92 taken 107 times.
214 query.bindValue(QStringLiteral(":uidplus"), uidPlusEvidence);
571
1/1
✓ Branch 101 → 102 taken 107 times.
214 query.bindValue(QStringLiteral(":updated"),
572 QDateTime::currentSecsSinceEpoch());
573
1/1
✓ Branch 110 → 111 taken 107 times.
214 query.bindValue(QStringLiteral(":token"), token);
574
6/6
✓ Branch 114 → 115 taken 107 times.
✓ Branch 115 → 116 taken 105 times.
✓ Branch 115 → 119 taken 2 times.
✓ Branch 116 → 117 taken 105 times.
✓ Branch 117 → 118 taken 104 times.
✓ Branch 117 → 119 taken 1 time.
107 const bool updated = query.exec() && query.numRowsAffected() == 1;
575
5/6
✓ Branch 120 → 121 taken 104 times.
✓ Branch 120 → 129 taken 3 times.
✓ Branch 121 → 122 taken 104 times.
✗ Branch 121 → 129 not taken.
✓ Branch 122 → 123 taken 40 times.
✓ Branch 122 → 129 taken 64 times.
107 if (updated && folderId >= 0 && mayChangeMoveExclusions)
576
1/1
✓ Branch 128 → 129 taken 40 times.
40 m_notify.mutationPhaseChanged(account, folderId, token, uid,
577
6/6
✓ Branch 123 → 124 taken 28 times.
✓ Branch 123 → 127 taken 12 times.
✓ Branch 124 → 125 taken 24 times.
✓ Branch 124 → 127 taken 4 times.
✓ Branch 125 → 126 taken 23 times.
✓ Branch 125 → 127 taken 1 time.
40 phase != MutationPhase::Confirmed && phase != MutationPhase::Failed &&
578 phase != MutationPhase::Quarantined);
579 107 return updated;
580 107 }
581
582 3213 bool MutationJournalStore::removeMutation(qint64 token, QList<qint64> *deferred) {
583
1/1
✓ Branch 2 → 3 taken 3213 times.
3213 QSqlQuery query(m_db);
584
1/1
✓ Branch 6 → 7 taken 3213 times.
3213 query.prepare(
585 6426 QStringLiteral("DELETE FROM mutation_journal WHERE token = :token"));
586
1/1
✓ Branch 14 → 15 taken 3213 times.
6426 query.bindValue(QStringLiteral(":token"), token);
587
6/6
✓ Branch 18 → 19 taken 3213 times.
✓ Branch 19 → 20 taken 3210 times.
✓ Branch 19 → 23 taken 3 times.
✓ Branch 20 → 21 taken 3210 times.
✓ Branch 21 → 22 taken 3209 times.
✓ Branch 21 → 23 taken 1 time.
3213 const bool removed = query.exec() && query.numRowsAffected() == 1;
588
5/5
✓ Branch 24 → 25 taken 3209 times.
✓ Branch 24 → 28 taken 4 times.
✓ Branch 25 → 26 taken 3148 times.
✓ Branch 25 → 28 taken 61 times.
✓ Branch 26 → 27 taken 3148 times.
3213 if (removed && deferred) deferred->append(token);
589
3/3
✓ Branch 28 → 29 taken 61 times.
✓ Branch 28 → 30 taken 4 times.
✓ Branch 29 → 30 taken 61 times.
65 else if (removed) m_notify.mutationRemoved(token);
590 3213 return removed;
591 3213 }
592
593 18 QList<qint64> MutationJournalStore::retireSourceFlagMutations(const MailId &source,
594 quint32 uidValidity) {
595 18 QList<qint64> tokens;
596
5/6
✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 5 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 17 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 17 times.
18 if (!source.isValid() || uidValidity == 0) return tokens;
597
1/1
✓ Branch 9 → 10 taken 17 times.
17 QSqlQuery query(m_db);
598
1/1
✓ Branch 13 → 14 taken 17 times.
17 query.prepare(QStringLiteral(
599 "DELETE FROM mutation_journal WHERE account_id = :account "
600 "AND folder_id = :folder AND uid = :uid AND uidvalidity = :validity "
601 "AND replay_class = :class AND kind IN (:seen, :flagged, :keyword) "
602 "RETURNING token"));
603
1/1
✓ Branch 21 → 22 taken 17 times.
34 query.bindValue(QStringLiteral(":account"), source.accountId);
604
1/1
✓ Branch 30 → 31 taken 17 times.
34 query.bindValue(QStringLiteral(":folder"), source.folderId);
605
1/1
✓ Branch 39 → 40 taken 17 times.
34 query.bindValue(QStringLiteral(":uid"), source.uid);
606
1/1
✓ Branch 48 → 49 taken 17 times.
34 query.bindValue(QStringLiteral(":validity"), uidValidity);
607
1/1
✓ Branch 57 → 58 taken 17 times.
34 query.bindValue(QStringLiteral(":class"), static_cast<int>(MutationReplayClass::DesiredState));
608
1/1
✓ Branch 66 → 67 taken 17 times.
34 query.bindValue(QStringLiteral(":seen"), static_cast<int>(MutationKind::Seen));
609
1/1
✓ Branch 75 → 76 taken 17 times.
34 query.bindValue(QStringLiteral(":flagged"), static_cast<int>(MutationKind::Flagged));
610
1/1
✓ Branch 84 → 85 taken 17 times.
34 query.bindValue(QStringLiteral(":keyword"), static_cast<int>(MutationKind::Keyword));
611
2/3
✓ Branch 88 → 89 taken 17 times.
✗ Branch 89 → 90 not taken.
✓ Branch 89 → 107 taken 17 times.
17 if (!query.exec()) {
612 ✗ qCWarning(lcCache) << "Could not retire moved-mail flag verification:" << query.lastError();
613 ✗ return tokens;
614 }
615
6/6
✓ Branch 108 → 109 taken 4 times.
✓ Branch 109 → 110 taken 4 times.
✓ Branch 110 → 111 taken 4 times.
✓ Branch 113 → 114 taken 21 times.
✓ Branch 114 → 108 taken 4 times.
✓ Branch 114 → 115 taken 17 times.
21 while (query.next()) tokens.append(query.value(0).toLongLong());
616
1/1
✓ Branch 115 → 116 taken 17 times.
17 query.finish();
617
5/5
✓ Branch 116 → 117 taken 17 times.
✓ Branch 117 → 118 taken 17 times.
✓ Branch 120 → 121 taken 4 times.
✓ Branch 123 → 119 taken 4 times.
✓ Branch 123 → 124 taken 17 times.
21 for (qint64 token : tokens) m_notify.mutationRemoved(token);
618 17 return tokens;
619 17 }
620
621 470 bool MutationJournalStore::journalUsage(int *count, qint64 *bytes) const {
622
1/1
✓ Branch 2 → 3 taken 470 times.
470 QSqlQuery query(m_db);
623
3/8
✓ Branch 14 → 15 taken 470 times.
✗ Branch 14 → 16 not taken.
✓ Branch 16 → 17 taken 6 times.
✓ Branch 16 → 18 taken 464 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
940 if (!query.exec(QStringLiteral(
624 "SELECT COUNT(*), COALESCE(SUM(128 + 2 * (length(account_id) + "
625 "length(folder_path) + length(flag_name) + length(target_folder) + "
626 "length(message_id) + length(uidplus_evidence) + length(error))), 0) "
627
4/5
✓ Branch 6 → 7 taken 470 times.
✓ Branch 7 → 8 taken 464 times.
✓ Branch 7 → 10 taken 6 times.
✓ Branch 12 → 13 taken 470 times.
✗ Branch 12 → 14 not taken.
1404 "FROM mutation_journal")) ||
628
2/3
✓ Branch 8 → 9 taken 464 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 464 times.
464 !query.next())
629 6 return false;
630
2/2
✓ Branch 18 → 19 taken 464 times.
✓ Branch 19 → 20 taken 464 times.
464 *count = query.value(0).toInt();
631
2/2
✓ Branch 21 → 22 taken 464 times.
✓ Branch 22 → 23 taken 464 times.
464 *bytes = query.value(1).toLongLong();
632 464 return true;
633 470 }
634
635 144 int MutationJournalStore::mutationCount() const {
636 144 int count = 0;
637 144 qint64 bytes = 0;
638
3/3
✓ Branch 2 → 3 taken 144 times.
✓ Branch 3 → 4 taken 141 times.
✓ Branch 3 → 5 taken 3 times.
288 return journalUsage(&count, &bytes) ? count : 0;
639 }
640
641 8 qint64 MutationJournalStore::mutationJournalBytes() const {
642 8 int count = 0;
643 8 qint64 bytes = 0;
644
3/3
✓ Branch 2 → 3 taken 8 times.
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 3 times.
16 return journalUsage(&count, &bytes) ? bytes : 0;
645 }
646