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.4% 62 / 0 / 65
Functions: 100.0% 8 / 0 / 8
Branches: 85.2% 98 / 0 / 115

data/MailCacheSql.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QSqlDatabase>
4 #include <QSqlDriver>
5 #include <QSqlError>
6 #include <QSqlQuery>
7 #include <QJsonArray>
8 #include <QJsonDocument>
9 #include <QRegularExpression>
10 #include <QString>
11 #include <QStringList>
12
13 #include "util/MessageId.h"
14 #include <sqlite3.h>
15
16 // SQL helpers shared by MailCache and the stores extracted from it
17 // (Sprint 91). Each store includes this header and brings the helper into
18 // its translation unit with `using MailCacheSql::…;` so moved bodies stay
19 // verbatim.
20 namespace MailCacheSql {
21
22 // Exact MIME bytes of journaled APPENDs (created by the schema and by the
23 // v22 migration).
24 inline const QString AppendPayloadSchema = QStringLiteral(
25 "CREATE TABLE IF NOT EXISTS append_payloads ("
26 "token INTEGER PRIMARY KEY REFERENCES mutation_journal(token) ON DELETE CASCADE,"
27 "message BLOB NOT NULL, flags TEXT NOT NULL, sha256 BLOB NOT NULL)");
28
29 // Write transactions must acquire the write lock up front (BEGIN IMMEDIATE).
30 // A deferred BEGIN lets an in-transaction read pin a WAL snapshot; if another
31 // connection commits after that snapshot was taken (the FolderPredictor and
32 // FTS rebuild background threadCaches share this database file), the
33 // read-to-write upgrade fails with SQLITE_BUSY immediately — a stale-snapshot
34 // upgrade bypasses busy_timeout (pipelines 5300/5304 lost cached headers this
35 // way). BEGIN IMMEDIATE makes concurrent writers serialize through the
36 // busy_timeout instead of aborting.
37 36969 inline bool beginWriteTransaction(QSqlDatabase &db, QString *errorOut = nullptr) {
38
1/1
✓ Branch 2 → 3 taken 36969 times.
36969 QSqlQuery begin(db);
39
1/1
✓ Branch 6 → 7 taken 36969 times.
36969 const bool started = begin.exec(QStringLiteral("BEGIN IMMEDIATE"));
40
6/6
✓ Branch 9 → 10 taken 37 times.
✓ Branch 9 → 17 taken 36932 times.
✓ Branch 10 → 11 taken 14 times.
✓ Branch 10 → 17 taken 23 times.
✓ Branch 11 → 12 taken 14 times.
✓ Branch 12 → 13 taken 14 times.
36969 if (!started && errorOut) *errorOut = begin.lastError().text();
41 36969 return started;
42 36969 }
43
44 // P90.3: flag writes compose with caller-owned transactions. On the
45 // autocommit path the read/update/aggregate-maintenance sequence gets its
46 // own short BEGIN IMMEDIATE unit (rolling back on scope exit); inside an
47 // already active transaction the statements simply join the caller's unit,
48 // matching the previous single-statement composability.
49 class ScopedWriteUnit {
50 public:
51 751 ScopedWriteUnit(QSqlDatabase &db, QString *errorOut = nullptr) : m_db(&db) {
52
2/2
✓ Branch 2 → 3 taken 751 times.
✓ Branch 3 → 4 taken 751 times.
751 const auto handle = db.driver()->handle();
53
14/21
✓ Branch 4 → 5 taken 751 times.
✓ Branch 5 → 6 taken 749 times.
✓ Branch 5 → 13 taken 2 times.
✓ Branch 8 → 9 taken 749 times.
✓ Branch 9 → 10 taken 749 times.
✓ Branch 11 → 12 taken 749 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 749 times.
✓ Branch 14 → 16 taken 2 times.
✓ Branch 16 → 17 taken 749 times.
✓ Branch 16 → 18 taken 2 times.
✓ Branch 18 → 19 taken 749 times.
✓ Branch 18 → 20 taken 2 times.
✓ Branch 20 → 21 taken 749 times.
✓ Branch 20 → 28 taken 2 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
751 if (handle.isValid() && QByteArray(handle.typeName()) == QByteArrayLiteral("sqlite3*")) {
54 749 auto *connection = *static_cast<sqlite3 **>(const_cast<void *>(handle.constData()));
55
4/5
✓ Branch 22 → 23 taken 749 times.
✗ Branch 22 → 26 not taken.
✓ Branch 23 → 24 taken 749 times.
✓ Branch 24 → 25 taken 195 times.
✓ Branch 24 → 26 taken 554 times.
749 m_joined = connection && sqlite3_get_autocommit(connection) == 0;
56 }
57
2/2
✓ Branch 28 → 29 taken 556 times.
✓ Branch 28 → 31 taken 195 times.
751 if (!m_joined) {
58
1/1
✓ Branch 29 → 30 taken 556 times.
556 m_owned = beginWriteTransaction(db, errorOut);
59 556 m_open = m_owned;
60 } else {
61 195 m_open = true;
62 }
63 751 }
64 751 ~ScopedWriteUnit() {
65
4/4
✓ Branch 2 → 3 taken 552 times.
✓ Branch 2 → 5 taken 199 times.
✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 5 taken 537 times.
751 if (m_owned && m_open) m_db->rollback();
66 751 }
67 751 bool started() const { return m_open; }
68 731 bool commit(QString *errorOut = nullptr) {
69
3/4
✓ Branch 2 → 3 taken 537 times.
✓ Branch 2 → 4 taken 194 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 537 times.
731 if (!m_owned || !m_open) return true;
70 537 m_open = false;
71
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 16 taken 536 times.
537 if (!m_db->commit()) {
72
3/4
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 14 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
1 if (errorOut) *errorOut = m_db->lastError().text();
73 1 m_db->rollback();
74 1 return false;
75 }
76 536 return true;
77 }
78 private:
79 QSqlDatabase *m_db = nullptr;
80 bool m_owned = false;
81 bool m_open = false;
82 bool m_joined = false;
83 };
84 // Canonical Message-ID token lists as stored in headers.in_reply_to/ref_ids.
85 1573623 inline QStringList canonicalThreadIds(const QStringList &ids) {
86 1573623 QStringList result;
87
1/1
✓ Branch 4 → 5 taken 1573623 times.
1573623 result.reserve(qMin<qsizetype>(ids.size(), MessageId::MaximumTokens));
88
2/2
✓ Branch 27 → 7 taken 562140 times.
✓ Branch 27 → 28 taken 1573623 times.
2135763 for (const QString &value : ids) {
89
5/5
✓ Branch 8 → 9 taken 562140 times.
✓ Branch 9 → 10 taken 562140 times.
✓ Branch 10 → 11 taken 562140 times.
✓ Branch 19 → 12 taken 562140 times.
✓ Branch 19 → 20 taken 562140 times.
1124280 for (const QString &token : MessageId::parseTokens(value)) {
90
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 562140 times.
562140 if (result.size() >= MessageId::MaximumTokens)
91 ✗ return result;
92
1/1
✓ Branch 16 → 17 taken 562140 times.
562140 result.append(token);
93 562140 }
94 }
95 1573623 return result;
96 ✗ }
97
98 827616 inline QString encodeThreadIds(const QStringList &ids) {
99
1/1
✓ Branch 2 → 3 taken 827616 times.
827616 QJsonArray array;
100
5/5
✓ Branch 3 → 4 taken 827616 times.
✓ Branch 4 → 5 taken 827616 times.
✓ Branch 5 → 6 taken 827616 times.
✓ Branch 13 → 7 taken 202965 times.
✓ Branch 13 → 14 taken 827616 times.
1030581 for (const QString &id : canonicalThreadIds(ids))
101
2/2
✓ Branch 8 → 9 taken 202965 times.
✓ Branch 9 → 10 taken 202965 times.
1030581 array.append(id);
102 return QString::fromUtf8(
103
3/3
✓ Branch 15 → 16 taken 827616 times.
✓ Branch 16 → 17 taken 827616 times.
✓ Branch 17 → 18 taken 827616 times.
1655232 QJsonDocument(array).toJson(QJsonDocument::Compact));
104 827616 }
105
106 746010 inline QStringList decodeThreadIds(const QString &stored) {
107
4/4
✓ Branch 2 → 3 taken 746010 times.
✓ Branch 5 → 6 taken 746010 times.
✓ Branch 7 → 8 taken 746006 times.
✓ Branch 7 → 41 taken 4 times.
746010 if (stored.trimmed().startsWith(QLatin1Char('['))) {
108 746006 QJsonParseError error;
109 const QJsonDocument document =
110
2/2
✓ Branch 8 → 9 taken 746006 times.
✓ Branch 9 → 10 taken 746006 times.
746006 QJsonDocument::fromJson(stored.toUtf8(), &error);
111
4/7
✓ Branch 11 → 12 taken 746006 times.
✗ Branch 11 → 15 not taken.
✓ Branch 12 → 13 taken 746006 times.
✓ Branch 13 → 14 taken 746006 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 746006 times.
✗ Branch 16 → 38 not taken.
746006 if (error.error == QJsonParseError::NoError && document.isArray()) {
112 746006 QStringList values;
113
6/6
✓ Branch 17 → 18 taken 746006 times.
✓ Branch 18 → 19 taken 746006 times.
✓ Branch 19 → 20 taken 746006 times.
✓ Branch 22 → 23 taken 359173 times.
✓ Branch 32 → 21 taken 359173 times.
✓ Branch 32 → 33 taken 746006 times.
1105179 for (const QJsonValue &value : document.array()) {
114
2/3
✓ Branch 23 → 24 taken 359173 times.
✓ Branch 24 → 25 taken 359173 times.
✗ Branch 24 → 29 not taken.
359173 if (value.isString())
115
2/2
✓ Branch 25 → 26 taken 359173 times.
✓ Branch 26 → 27 taken 359173 times.
359173 values.append(value.toString());
116 1105179 }
117
1/1
✓ Branch 34 → 35 taken 746006 times.
746006 return canonicalThreadIds(values);
118 746006 }
119 ✗ return {};
120 746006 }
121
122
1/1
✓ Branch 41 → 42 taken 4 times.
4 QStringList parsed = MessageId::parseTokens(stored);
123
9/11
✓ Branch 43 → 44 taken 3 times.
✓ Branch 43 → 47 taken 1 time.
✓ Branch 44 → 45 taken 3 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 48 taken 1 time.
✓ Branch 49 → 50 taken 3 times.
✓ Branch 49 → 51 taken 1 time.
✓ Branch 51 → 52 taken 3 times.
✓ Branch 51 → 53 taken 1 time.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
4 if (!parsed.isEmpty() || stored.trimmed().isEmpty())
124 3 return parsed;
125
126 // v10-v18 stored bare canonical IDs separated by spaces. This cannot
127 // represent quoted local parts containing whitespace, but those old rows
128 // cannot be reconstructed because raw headers were never cached.
129
2/2
✓ Branch 58 → 59 taken 1 time.
✓ Branch 59 → 60 taken 1 time.
3 return canonicalThreadIds(stored.split(QRegularExpression(QStringLiteral("\\s+")),
130
1/1
✓ Branch 60 → 61 taken 1 time.
1 Qt::SkipEmptyParts));
131 4 }
132
133 } // namespace MailCacheSql
134