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.9% 686 / 0 / 715
Functions: 90.5% 57 / 0 / 63
Branches: 87.2% 1135 / 0 / 1302

data/ContactStore.cpp
Line Branch Exec Source
1 #include "ContactStore.h"
2
3 #include "ContactIndex.h"
4 #include "ContactPeopleStore.h"
5 #include "ContactSqlValue.h"
6 #include "DatabaseSecurity.h"
7 #include "VCardCodec.h"
8
9 #include <QFile>
10 #include <QLoggingCategory>
11 #include <QSet>
12 #include <QSqlError>
13 #include <QSqlQuery>
14 #include <QUuid>
15
16
4/7
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 8 taken 47 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 9 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
56 Q_LOGGING_CATEGORY(lcContactStore, "mailjd.contactstore")
17
18 namespace {
19
20 const char *kCardColumns = "id, book_id, uid, href, etag, raw, base_raw, sync_state, writable";
21 const char *kBookColumns =
22 "id, account_id, path, display_name, read_only, ctag, sync_token, supports_sync_collection, "
23 "supported_versions, max_resource_size, fetched, last_sync, last_error, show_in_all, use_for_completion, "
24 "skipped_cards";
25 const char *kOutboxColumns =
26 "id, book_id, uid, operation, href, base_etag, revision, attempts, next_attempt, last_error, state, "
27 "server_raw, server_etag, conflict_fields, depends_on, base_raw";
28
29 2791 StoredCard cardFrom(const QSqlQuery &q) {
30 2791 StoredCard card;
31
2/2
✓ Branch 2 → 3 taken 2791 times.
✓ Branch 3 → 4 taken 2791 times.
2791 card.id = q.value(0).toLongLong();
32
2/2
✓ Branch 5 → 6 taken 2791 times.
✓ Branch 6 → 7 taken 2791 times.
2791 card.bookId = q.value(1).toLongLong();
33
2/2
✓ Branch 8 → 9 taken 2791 times.
✓ Branch 9 → 10 taken 2791 times.
2791 card.uid = q.value(2).toString();
34
2/2
✓ Branch 13 → 14 taken 2791 times.
✓ Branch 14 → 15 taken 2791 times.
2791 card.href = q.value(3).toString();
35
2/2
✓ Branch 18 → 19 taken 2791 times.
✓ Branch 19 → 20 taken 2791 times.
2791 card.etag = q.value(4).toString();
36
2/2
✓ Branch 23 → 24 taken 2791 times.
✓ Branch 24 → 25 taken 2791 times.
2791 card.raw = q.value(5).toString();
37
2/2
✓ Branch 28 → 29 taken 2791 times.
✓ Branch 29 → 30 taken 2791 times.
2791 card.baseRaw = q.value(6).toString();
38
2/2
✓ Branch 33 → 34 taken 2791 times.
✓ Branch 34 → 35 taken 2791 times.
2791 card.syncState = q.value(7).toString();
39
2/2
✓ Branch 38 → 39 taken 2791 times.
✓ Branch 39 → 40 taken 2791 times.
2791 card.writable = q.value(8).toBool();
40 2791 return card;
41 ✗ }
42
43 25433 AddressBookInfo bookFrom(const QSqlQuery &q) {
44 25433 AddressBookInfo book;
45
2/2
✓ Branch 3 → 4 taken 25433 times.
✓ Branch 4 → 5 taken 25433 times.
25433 book.id = q.value(0).toLongLong();
46
2/2
✓ Branch 6 → 7 taken 25433 times.
✓ Branch 7 → 8 taken 25433 times.
25433 book.accountId = q.value(1).toString();
47
2/2
✓ Branch 11 → 12 taken 25433 times.
✓ Branch 12 → 13 taken 25433 times.
25433 book.path = q.value(2).toString();
48
2/2
✓ Branch 16 → 17 taken 25433 times.
✓ Branch 17 → 18 taken 25433 times.
25433 book.displayName = q.value(3).toString();
49
2/2
✓ Branch 21 → 22 taken 25433 times.
✓ Branch 22 → 23 taken 25433 times.
25433 book.readOnly = q.value(4).toBool();
50
2/2
✓ Branch 24 → 25 taken 25433 times.
✓ Branch 25 → 26 taken 25433 times.
25433 book.ctag = q.value(5).toString();
51
2/2
✓ Branch 29 → 30 taken 25433 times.
✓ Branch 30 → 31 taken 25433 times.
25433 book.syncToken = q.value(6).toString();
52
2/2
✓ Branch 34 → 35 taken 25433 times.
✓ Branch 35 → 36 taken 25433 times.
25433 book.supportsSyncCollection = q.value(7).toBool();
53
3/3
✓ Branch 37 → 38 taken 25433 times.
✓ Branch 38 → 39 taken 25433 times.
✓ Branch 42 → 43 taken 25433 times.
25433 book.supportedVersions = q.value(8).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
54
2/2
✓ Branch 47 → 48 taken 25433 times.
✓ Branch 48 → 49 taken 25433 times.
25433 book.maxResourceSize = q.value(9).toLongLong();
55
2/2
✓ Branch 50 → 51 taken 25433 times.
✓ Branch 51 → 52 taken 25433 times.
25433 book.fetched = q.value(10).toBool();
56
3/3
✓ Branch 53 → 54 taken 25433 times.
✓ Branch 54 → 55 taken 25433 times.
✓ Branch 55 → 56 taken 25433 times.
25433 book.lastSync = QDateTime::fromString(q.value(11).toString(), Qt::ISODate);
57
2/2
✓ Branch 60 → 61 taken 25433 times.
✓ Branch 61 → 62 taken 25433 times.
25433 book.lastError = q.value(12).toString();
58
2/2
✓ Branch 65 → 66 taken 25433 times.
✓ Branch 66 → 67 taken 25433 times.
25433 book.showInAll = q.value(13).toBool();
59
2/2
✓ Branch 68 → 69 taken 25433 times.
✓ Branch 69 → 70 taken 25433 times.
25433 book.useForCompletion = q.value(14).toBool();
60
2/2
✓ Branch 71 → 72 taken 25433 times.
✓ Branch 72 → 73 taken 25433 times.
25433 book.skippedCards = q.value(15).toInt();
61 25433 return book;
62 ✗ }
63
64 1940 ContactOutboxEntry outboxFrom(const QSqlQuery &q) {
65 1940 ContactOutboxEntry entry;
66
2/2
✓ Branch 3 → 4 taken 1940 times.
✓ Branch 4 → 5 taken 1940 times.
1940 entry.id = q.value(0).toLongLong();
67
2/2
✓ Branch 6 → 7 taken 1940 times.
✓ Branch 7 → 8 taken 1940 times.
1940 entry.bookId = q.value(1).toLongLong();
68
2/2
✓ Branch 9 → 10 taken 1940 times.
✓ Branch 10 → 11 taken 1940 times.
1940 entry.uid = q.value(2).toString();
69
2/2
✓ Branch 14 → 15 taken 1940 times.
✓ Branch 15 → 16 taken 1940 times.
1940 entry.operation = q.value(3).toString();
70
2/2
✓ Branch 19 → 20 taken 1940 times.
✓ Branch 20 → 21 taken 1940 times.
1940 entry.href = q.value(4).toString();
71
2/2
✓ Branch 24 → 25 taken 1940 times.
✓ Branch 25 → 26 taken 1940 times.
1940 entry.baseEtag = q.value(5).toString();
72
2/2
✓ Branch 29 → 30 taken 1940 times.
✓ Branch 30 → 31 taken 1940 times.
1940 entry.revision = q.value(6).toInt();
73
2/2
✓ Branch 32 → 33 taken 1940 times.
✓ Branch 33 → 34 taken 1940 times.
1940 entry.attempts = q.value(7).toInt();
74
2/2
✓ Branch 35 → 36 taken 1940 times.
✓ Branch 36 → 37 taken 1940 times.
1940 entry.nextAttempt = q.value(8).toLongLong();
75
2/2
✓ Branch 38 → 39 taken 1940 times.
✓ Branch 39 → 40 taken 1940 times.
1940 entry.lastError = q.value(9).toString();
76
2/2
✓ Branch 43 → 44 taken 1940 times.
✓ Branch 44 → 45 taken 1940 times.
1940 entry.state = q.value(10).toString();
77
2/2
✓ Branch 48 → 49 taken 1940 times.
✓ Branch 49 → 50 taken 1940 times.
1940 entry.serverRaw = q.value(11).toString();
78
2/2
✓ Branch 53 → 54 taken 1940 times.
✓ Branch 54 → 55 taken 1940 times.
1940 entry.serverEtag = q.value(12).toString();
79
3/3
✓ Branch 58 → 59 taken 1940 times.
✓ Branch 59 → 60 taken 1940 times.
✓ Branch 63 → 64 taken 1940 times.
1940 entry.conflictFields = q.value(13).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
80
2/2
✓ Branch 68 → 69 taken 1940 times.
✓ Branch 69 → 70 taken 1940 times.
1940 entry.dependsOn = q.value(14).toLongLong();
81
2/2
✓ Branch 71 → 72 taken 1940 times.
✓ Branch 72 → 73 taken 1940 times.
1940 entry.baseRaw = q.value(15).toString();
82 1940 return entry;
83 ✗ }
84
85 60856 bool exec(QSqlDatabase &db, const QString &sql) {
86
1/1
✓ Branch 2 → 3 taken 60856 times.
60856 QSqlQuery q(db);
87
3/3
✓ Branch 3 → 4 taken 60856 times.
✓ Branch 4 → 5 taken 60837 times.
✓ Branch 4 → 6 taken 19 times.
60856 if (q.exec(sql))
88 60837 return true;
89
10/10
✓ Branch 6 → 7 taken 19 times.
✓ Branch 10 → 11 taken 19 times.
✓ Branch 11 → 12 taken 19 times.
✓ Branch 12 → 13 taken 19 times.
✓ Branch 13 → 14 taken 19 times.
✓ Branch 14 → 15 taken 19 times.
✓ Branch 15 → 16 taken 19 times.
✓ Branch 16 → 17 taken 19 times.
✓ Branch 23 → 8 taken 19 times.
✓ Branch 23 → 24 taken 19 times.
38 qCWarning(lcContactStore) << "Schema statement failed:" << q.lastError().text() << sql.left(60);
90 19 return false;
91 60856 }
92
93 const QStringList kSchema{
94 QStringLiteral("CREATE TABLE IF NOT EXISTS address_books ("
95 " id INTEGER PRIMARY KEY AUTOINCREMENT, account_id TEXT NOT NULL DEFAULT '',"
96 " path TEXT NOT NULL, display_name TEXT NOT NULL DEFAULT '', read_only INTEGER NOT NULL DEFAULT 0,"
97 " ctag TEXT NOT NULL DEFAULT '', sync_token TEXT NOT NULL DEFAULT '',"
98 " supports_sync_collection INTEGER NOT NULL DEFAULT 0, supported_versions TEXT NOT NULL DEFAULT '',"
99 " max_resource_size INTEGER NOT NULL DEFAULT 0, fetched INTEGER NOT NULL DEFAULT 0,"
100 " last_sync TEXT NOT NULL DEFAULT '', last_error TEXT NOT NULL DEFAULT '',"
101 " show_in_all INTEGER NOT NULL DEFAULT 1, use_for_completion INTEGER NOT NULL DEFAULT 1,"
102 " skipped_cards INTEGER NOT NULL DEFAULT 0, UNIQUE(account_id, path))"),
103 QStringLiteral("CREATE TABLE IF NOT EXISTS cards ("
104 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
105 " book_id INTEGER NOT NULL REFERENCES address_books(id) ON DELETE CASCADE,"
106 " uid TEXT NOT NULL, href TEXT NOT NULL DEFAULT '', etag TEXT NOT NULL DEFAULT '',"
107 " raw TEXT NOT NULL, base_raw TEXT NOT NULL DEFAULT '', sync_state TEXT NOT NULL DEFAULT '',"
108 " writable INTEGER NOT NULL DEFAULT 1, version TEXT NOT NULL DEFAULT '',"
109 " kind TEXT NOT NULL DEFAULT 'individual', display_name TEXT NOT NULL DEFAULT '',"
110 " sort_display TEXT NOT NULL DEFAULT '', sort_family TEXT NOT NULL DEFAULT '',"
111 " section TEXT NOT NULL DEFAULT '#', organization TEXT NOT NULL DEFAULT '',"
112 " primary_address TEXT NOT NULL DEFAULT '', address_count INTEGER NOT NULL DEFAULT 0,"
113 " has_photo INTEGER NOT NULL DEFAULT 0, photo_thumb BLOB,"
114 " search_text TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT (datetime('now')),"
115 " UNIQUE(book_id, uid))"),
116 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_cards_sort ON cards(sort_display, id)"),
117 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_cards_family ON cards(sort_family, id)"),
118 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_cards_href ON cards(book_id, href)"),
119 // Group cards are few; groups() runs per completion keystroke and per
120 // sidebar refresh and must not scan every card (G10).
121 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_cards_group_kind ON cards(book_id) WHERE kind = 'group'"),
122 QStringLiteral("CREATE TABLE IF NOT EXISTS card_addresses ("
123 " card_id INTEGER NOT NULL REFERENCES cards(id) ON DELETE CASCADE, position INTEGER NOT NULL,"
124 " address TEXT NOT NULL, address_key TEXT NOT NULL, type TEXT NOT NULL DEFAULT '',"
125 " preferred INTEGER NOT NULL DEFAULT 0)"),
126 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_card_addresses_key ON card_addresses(address_key)"),
127 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_card_addresses_card ON card_addresses(card_id)"),
128 QStringLiteral("CREATE TABLE IF NOT EXISTS card_groups ("
129 " card_id INTEGER NOT NULL REFERENCES cards(id) ON DELETE CASCADE, name TEXT NOT NULL)"),
130 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_card_groups_card ON card_groups(card_id)"),
131 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_card_groups_name ON card_groups(name)"),
132 QStringLiteral("CREATE TABLE IF NOT EXISTS card_members ("
133 " card_id INTEGER NOT NULL REFERENCES cards(id) ON DELETE CASCADE, member TEXT NOT NULL)"),
134 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_card_members_card ON card_members(card_id)"),
135 QStringLiteral("CREATE TABLE IF NOT EXISTS collected_addresses ("
136 " address_key TEXT PRIMARY KEY, address TEXT NOT NULL, display_name TEXT NOT NULL DEFAULT '',"
137 " first_used TEXT NOT NULL DEFAULT '', last_used TEXT NOT NULL DEFAULT '')"),
138 QStringLiteral("CREATE TABLE IF NOT EXISTS address_usage ("
139 " address_key TEXT PRIMARY KEY, use_count INTEGER NOT NULL DEFAULT 0,"
140 " last_used TEXT NOT NULL DEFAULT '')"),
141 QStringLiteral("CREATE TABLE IF NOT EXISTS contact_outbox ("
142 " id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, uid TEXT NOT NULL,"
143 " operation TEXT NOT NULL, href TEXT NOT NULL DEFAULT '', base_etag TEXT NOT NULL DEFAULT '',"
144 " revision INTEGER NOT NULL DEFAULT 1, attempts INTEGER NOT NULL DEFAULT 0,"
145 " next_attempt INTEGER NOT NULL DEFAULT 0, last_error TEXT NOT NULL DEFAULT '',"
146 " state TEXT NOT NULL DEFAULT 'pending', server_raw TEXT NOT NULL DEFAULT '',"
147 " server_etag TEXT NOT NULL DEFAULT '', conflict_fields TEXT NOT NULL DEFAULT '',"
148 " depends_on INTEGER NOT NULL DEFAULT 0, base_raw TEXT NOT NULL DEFAULT '',"
149 " UNIQUE(book_id, uid))"),
150 // Sprint 107 D-21: the losing side of conflict resolutions.
151 ConflictHistory::createStatement(),
152 QStringLiteral("CREATE TABLE IF NOT EXISTS contact_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '')"),
153 };
154
155 } // namespace
156
157
1/1
✓ Branch 3 → 4 taken 4336 times.
4336 ContactStore::ContactStore(QObject *parent) : QObject(parent) {}
158
159 5323 ContactStore::~ContactStore() { close(); }
160
161 3021 bool ContactStore::open(const QString &dbPath) {
162
3/3
✓ Branch 2 → 3 taken 3021 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 16 taken 3018 times.
3021 if (!DatabaseSecurity::preparePath(dbPath)) {
163
6/6
✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 14 → 6 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
6 qCWarning(lcContactStore) << "Failed to create private contacts DB:" << dbPath;
164 3 return false;
165 }
166 3018 m_dbPath = dbPath;
167
3/3
✓ Branch 17 → 18 taken 3018 times.
✓ Branch 18 → 19 taken 3018 times.
✓ Branch 22 → 23 taken 3018 times.
6036 m_connectionName = QStringLiteral("contacts_") + QUuid::createUuid().toString();
168
2/2
✓ Branch 31 → 32 taken 3018 times.
✓ Branch 32 → 33 taken 3018 times.
6036 m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
169
1/1
✓ Branch 36 → 37 taken 3018 times.
3018 m_db.setDatabaseName(dbPath);
170
2/3
✓ Branch 37 → 38 taken 3018 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 55 taken 3018 times.
3018 if (!m_db.open()) {
171 ✗ qCWarning(lcContactStore) << "Failed to open contacts DB:" << m_db.lastError().text();
172 ✗ return false;
173 }
174
2/3
✓ Branch 55 → 56 taken 3018 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 70 taken 3018 times.
3018 if (!DatabaseSecurity::restrictExistingFile(dbPath)) {
175 ✗ qCWarning(lcContactStore) << "Failed to restrict contacts DB permissions:" << dbPath;
176 ✗ m_db.close();
177 ✗ return false;
178 }
179
1/1
✓ Branch 70 → 71 taken 3018 times.
3018 QSqlQuery pragma(m_db);
180
1/1
✓ Branch 74 → 75 taken 3018 times.
3018 pragma.exec(QStringLiteral("PRAGMA journal_mode=WAL"));
181
1/1
✓ Branch 80 → 81 taken 3018 times.
3018 pragma.exec(QStringLiteral("PRAGMA synchronous=NORMAL"));
182 // T-619/SEC-19: Prevent SQLITE_BUSY errors during concurrent access
183
1/1
✓ Branch 86 → 87 taken 3018 times.
3018 pragma.exec(QStringLiteral("PRAGMA busy_timeout=5000"));
184
1/1
✓ Branch 92 → 93 taken 3018 times.
3018 pragma.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
185
1/1
✓ Branch 95 → 96 taken 3018 times.
3018 return createSchema();
186 3018 }
187
188 4415 void ContactStore::close() {
189
2/2
✓ Branch 3 → 4 taken 3018 times.
✓ Branch 3 → 5 taken 1397 times.
4415 if (m_db.isOpen())
190 3018 m_db.close();
191 // T-402/Bug 20: Release reference before removeDatabase
192
2/2
✓ Branch 5 → 6 taken 4415 times.
✓ Branch 6 → 7 taken 4415 times.
4415 m_db = QSqlDatabase();
193
2/2
✓ Branch 9 → 10 taken 3018 times.
✓ Branch 9 → 12 taken 1397 times.
4415 if (!m_connectionName.isEmpty()) {
194 3018 QSqlDatabase::removeDatabase(m_connectionName);
195 3018 m_connectionName.clear();
196 }
197 4415 }
198
199 3793 bool ContactStore::isOpen() const { return m_db.isOpen(); }
200
201 163 bool ContactStore::beginTransaction() { return m_db.transaction(); }
202
203 160 bool ContactStore::commitTransaction() { return m_db.commit(); }
204
205 3 void ContactStore::rollbackTransaction() { m_db.rollback(); }
206
207 3018 bool ContactStore::createSchema() {
208
1/1
✓ Branch 2 → 3 taken 3018 times.
3018 QSqlQuery version(m_db);
209 3018 int current = 0;
210
9/16
✓ Branch 6 → 7 taken 3018 times.
✓ Branch 7 → 8 taken 3017 times.
✓ Branch 7 → 11 taken 1 time.
✓ Branch 8 → 9 taken 3017 times.
✓ Branch 9 → 10 taken 3017 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 3018 times.
✗ Branch 12 → 14 not taken.
✓ Branch 14 → 15 taken 3018 times.
✗ Branch 14 → 16 not taken.
✓ Branch 16 → 17 taken 3017 times.
✓ Branch 16 → 21 taken 1 time.
✗ Branch 212 → 213 not taken.
✗ Branch 212 → 214 not taken.
✗ Branch 216 → 217 not taken.
✗ Branch 216 → 218 not taken.
6036 if (version.exec(QStringLiteral("PRAGMA user_version")) && version.next())
211
2/2
✓ Branch 17 → 18 taken 3017 times.
✓ Branch 18 → 19 taken 3017 times.
3017 current = version.value(0).toInt();
212
1/1
✓ Branch 21 → 22 taken 3018 times.
3018 version.finish();
213
2/2
✓ Branch 22 → 23 taken 2576 times.
✓ Branch 22 → 57 taken 442 times.
3018 if (current >= SchemaVersion) {
214 // F-77 (SPRINT-104): the same-version path runs in a transaction too
215 // — its people-table rebuild (an interim v6 build without place_name)
216 // drops and backfills derived tables; interrupted outside a
217 // transaction, the shape check passed forever after while every
218 // people row of an untouched card stayed invisible. A failure now
219 // rolls the rebuild back as a whole.
220
2/3
✓ Branch 23 → 24 taken 2576 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 2576 times.
2576 if (!m_db.transaction())
221 ✗ return false;
222 2576 bool ok = true;
223
2/2
✓ Branch 36 → 28 taken 48944 times.
✓ Branch 36 → 37 taken 2576 times.
51520 for (const QString &statement : kSchema)
224
3/5
✓ Branch 29 → 30 taken 48944 times.
✓ Branch 30 → 31 taken 48944 times.
✗ Branch 30 → 33 not taken.
✓ Branch 31 → 32 taken 48944 times.
✗ Branch 31 → 33 not taken.
48944 ok = exec(m_db, statement) && ok; // additive, idempotent; a failure is reported (S106-166)
225
4/5
✓ Branch 37 → 38 taken 2576 times.
✗ Branch 37 → 41 not taken.
✓ Branch 38 → 39 taken 2576 times.
✓ Branch 39 → 40 taken 2575 times.
✓ Branch 39 → 41 taken 1 time.
2576 ok = ok && ContactPeopleStore::createSchema(m_db);
226
2/2
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 55 taken 2575 times.
2576 if (!ok) {
227
5/5
✓ Branch 43 → 44 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 52 → 45 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
2 qCWarning(lcContactStore) << "Contact schema check failed; rolled back";
228
1/1
✓ Branch 53 → 54 taken 1 time.
1 m_db.rollback();
229 1 return false;
230 }
231
1/1
✓ Branch 55 → 56 taken 2575 times.
2575 return m_db.commit();
232 }
233 442 bool hasLegacy = false;
234 {
235
1/1
✓ Branch 57 → 58 taken 442 times.
442 QSqlQuery tables(m_db);
236
1/1
✓ Branch 61 → 62 taken 442 times.
442 tables.exec(QStringLiteral("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'contacts'"));
237
1/1
✓ Branch 64 → 65 taken 442 times.
442 hasLegacy = tables.next();
238 442 }
239 // Only the first schema (0 → v1) converts the pre-Sprint-97 table; a
240 // later step must not convert it again while it still waits to be dropped.
241
4/4
✓ Branch 66 → 67 taken 8 times.
✓ Branch 66 → 69 taken 434 times.
✓ Branch 67 → 68 taken 7 times.
✓ Branch 67 → 69 taken 1 time.
442 const bool convertLegacy = hasLegacy && current == 0;
242
4/4
✓ Branch 70 → 71 taken 4 times.
✓ Branch 70 → 73 taken 438 times.
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 73 taken 2 times.
442 const bool backfillPeople = current > 0 && current < 6; // S103.1.4: derive the v6 people rows once
243
7/8
✓ Branch 74 → 75 taken 435 times.
✓ Branch 74 → 76 taken 7 times.
✓ Branch 75 → 76 taken 2 times.
✓ Branch 75 → 79 taken 433 times.
✓ Branch 77 → 78 taken 9 times.
✗ Branch 77 → 79 not taken.
✓ Branch 80 → 81 taken 9 times.
✓ Branch 80 → 150 taken 433 times.
442 if ((convertLegacy || backfillPeople) && !m_dbPath.isEmpty()) {
244 // Pre-update backup; .pre-v1 is removed after the old table is dropped.
245
7/11
✓ Branch 81 → 82 taken 7 times.
✓ Branch 81 → 85 taken 2 times.
✓ Branch 88 → 89 taken 9 times.
✓ Branch 90 → 91 taken 2 times.
✓ Branch 90 → 92 taken 7 times.
✓ Branch 92 → 93 taken 7 times.
✓ Branch 92 → 94 taken 2 times.
✗ Branch 240 → 241 not taken.
✗ Branch 240 → 242 not taken.
✗ Branch 244 → 245 not taken.
✗ Branch 244 → 246 not taken.
18 const QString backup = m_dbPath + (convertLegacy ? QStringLiteral(".pre-v1") : QStringLiteral(".pre-v6"));
246
1/1
✓ Branch 94 → 95 taken 9 times.
9 QSqlQuery checkpoint(m_db);
247
1/1
✓ Branch 98 → 99 taken 9 times.
9 checkpoint.exec(QStringLiteral("PRAGMA wal_checkpoint(TRUNCATE)"));
248 // S106-166: the new copy is complete before an older backup goes.
249
1/1
✓ Branch 104 → 105 taken 9 times.
9 const QString fresh = backup + QStringLiteral(".new");
250
1/1
✓ Branch 107 → 108 taken 9 times.
9 QFile::remove(fresh);
251
3/4
✓ Branch 108 → 109 taken 9 times.
✓ Branch 110 → 111 taken 9 times.
✓ Branch 111 → 112 taken 9 times.
✗ Branch 111 → 119 not taken.
18 if (QFile::copy(m_dbPath, fresh) && DatabaseSecurity::restrictExistingFile(fresh) &&
252
6/13
✓ Branch 109 → 110 taken 9 times.
✗ Branch 109 → 119 not taken.
✓ Branch 112 → 113 taken 9 times.
✗ Branch 113 → 114 not taken.
✓ Branch 113 → 116 taken 9 times.
✗ Branch 114 → 115 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 119 not taken.
✓ Branch 116 → 117 taken 9 times.
✓ Branch 117 → 118 taken 9 times.
✗ Branch 117 → 119 not taken.
✓ Branch 120 → 121 taken 9 times.
✗ Branch 120 → 133 not taken.
18 (!QFile::exists(backup) || QFile::remove(backup)) && QFile::rename(fresh, backup)) {
253
6/6
✓ Branch 121 → 122 taken 9 times.
✓ Branch 125 → 126 taken 9 times.
✓ Branch 126 → 127 taken 9 times.
✓ Branch 127 → 128 taken 9 times.
✓ Branch 131 → 123 taken 9 times.
✓ Branch 131 → 132 taken 9 times.
18 qCInfo(lcContactStore) << "Pre-migration backup written:" << backup;
254 } else {
255 ✗ QFile::remove(fresh);
256 ✗ qCWarning(lcContactStore) << "Pre-migration backup could not be written:" << backup;
257 }
258 9 }
259
2/3
✓ Branch 150 → 151 taken 442 times.
✗ Branch 151 → 152 not taken.
✓ Branch 151 → 153 taken 442 times.
442 if (!m_db.transaction())
260 ✗ return false;
261 442 bool ok = true;
262
2/2
✓ Branch 163 → 155 taken 8398 times.
✓ Branch 163 → 164 taken 442 times.
8840 for (const QString &statement : kSchema)
263
4/5
✓ Branch 156 → 157 taken 8398 times.
✓ Branch 157 → 158 taken 8379 times.
✓ Branch 157 → 160 taken 19 times.
✓ Branch 158 → 159 taken 8379 times.
✗ Branch 158 → 160 not taken.
8398 ok = exec(m_db, statement) && ok;
264
6/8
✓ Branch 164 → 165 taken 441 times.
✓ Branch 164 → 170 taken 1 time.
✓ Branch 165 → 166 taken 441 times.
✓ Branch 166 → 167 taken 441 times.
✗ Branch 166 → 170 not taken.
✓ Branch 167 → 168 taken 441 times.
✓ Branch 168 → 169 taken 441 times.
✗ Branch 168 → 170 not taken.
442 ok = ok && addMissingColumns() && ContactPeopleStore::createSchema(m_db);
265
6/7
✓ Branch 171 → 172 taken 441 times.
✓ Branch 171 → 176 taken 1 time.
✓ Branch 172 → 173 taken 2 times.
✓ Branch 172 → 175 taken 439 times.
✓ Branch 173 → 174 taken 2 times.
✓ Branch 174 → 175 taken 2 times.
✗ Branch 174 → 176 not taken.
442 ok = ok && (!backfillPeople || ContactPeopleStore::backfill(m_db));
266
4/4
✓ Branch 177 → 178 taken 441 times.
✓ Branch 177 → 181 taken 1 time.
✓ Branch 178 → 179 taken 7 times.
✓ Branch 178 → 181 taken 434 times.
442 if (ok && convertLegacy)
267
1/1
✓ Branch 179 → 180 taken 7 times.
7 ok = convertLegacyContacts();
268
2/2
✓ Branch 181 → 182 taken 441 times.
✓ Branch 181 → 192 taken 1 time.
442 if (ok)
269
2/2
✓ Branch 186 → 187 taken 441 times.
✓ Branch 187 → 188 taken 441 times.
882 ok = exec(m_db, QStringLiteral("PRAGMA user_version = %1").arg(SchemaVersion));
270
2/2
✓ Branch 192 → 193 taken 1 time.
✓ Branch 192 → 207 taken 441 times.
442 if (!ok) {
271
1/1
✓ Branch 193 → 194 taken 1 time.
1 m_db.rollback();
272
7/7
✓ Branch 194 → 195 taken 1 time.
✓ Branch 198 → 199 taken 1 time.
✓ Branch 199 → 200 taken 1 time.
✓ Branch 200 → 201 taken 1 time.
✓ Branch 201 → 202 taken 1 time.
✓ Branch 205 → 196 taken 1 time.
✓ Branch 205 → 206 taken 1 time.
2 qCWarning(lcContactStore) << "Contact schema version" << SchemaVersion << "could not be created";
273 1 return false;
274 }
275
1/1
✓ Branch 207 → 208 taken 441 times.
441 return m_db.commit();
276 3018 }
277
278 // Columns added after v1 (CREATE TABLE IF NOT EXISTS keeps an existing
279 // table): v2 (S97.3.1) `skipped_cards` separates unreadable cards from a
280 // failed sync (`last_error`); v3 (S97.5.10) `cards.birthday`; v4 (S97.5.7)
281 // `cards.photo_uri` and `photo_fetched_uri`; v5 (F100-10) `phone`,
282 // `job_title`, `locality`; v7 `anniversary`.
283 441 bool ContactStore::addMissingColumns() {
284 1323 const auto columns = [this](const QString &table) {
285
1/1
✓ Branch 2 → 3 taken 1323 times.
1323 QSqlQuery info(m_db);
286 1323 QStringList names;
287
3/4
✓ Branch 7 → 8 taken 1323 times.
✓ Branch 8 → 9 taken 1323 times.
✓ Branch 12 → 13 taken 1323 times.
✗ Branch 12 → 22 not taken.
2646 if (info.exec(QStringLiteral("PRAGMA table_info(%1)").arg(table)))
288
3/3
✓ Branch 20 → 21 taken 25162 times.
✓ Branch 21 → 14 taken 23839 times.
✓ Branch 21 → 22 taken 1323 times.
25162 while (info.next())
289
3/3
✓ Branch 14 → 15 taken 23839 times.
✓ Branch 15 → 16 taken 23839 times.
✓ Branch 16 → 17 taken 23839 times.
23839 names.append(info.value(1).toString());
290 1323 return names;
291 1323 };
292 441 bool ok = true;
293
3/3
✓ Branch 5 → 6 taken 441 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 19 taken 440 times.
441 if (!columns(QStringLiteral("address_books")).contains(QLatin1String("skipped_cards")))
294
1/1
✓ Branch 15 → 16 taken 1 time.
1 ok = exec(m_db, QStringLiteral("ALTER TABLE address_books ADD COLUMN skipped_cards INTEGER NOT NULL DEFAULT 0"));
295
1/1
✓ Branch 22 → 23 taken 441 times.
441 const QStringList cardColumns = columns(QStringLiteral("cards"));
296 // v3 (S97.5.10): `birthday` ("YYYY-MM-DD" / "--MM-DD") for the calendar's
297 // birthday layer.
298
5/6
✓ Branch 25 → 26 taken 441 times.
✗ Branch 25 → 30 not taken.
✓ Branch 28 → 29 taken 437 times.
✓ Branch 28 → 30 taken 4 times.
✓ Branch 31 → 32 taken 437 times.
✓ Branch 31 → 41 taken 4 times.
441 if (ok && !cardColumns.contains(QLatin1String("birthday")))
299
1/1
✓ Branch 36 → 37 taken 437 times.
874 ok = addDerivedColumn(QStringLiteral("birthday"), [](const ContactCard &card) {
300 ✗ return card.birthday.isValid() ? card.birthday.toIso() : QString();
301 });
302 // v4 (S97.5.7): `photo_uri` of a URI PHOTO; `photo_fetched_uri` names the
303 // URI `photo_thumb` was fetched (or tried) for.
304
5/6
✓ Branch 41 → 42 taken 441 times.
✗ Branch 41 → 46 not taken.
✓ Branch 44 → 45 taken 437 times.
✓ Branch 44 → 46 taken 4 times.
✓ Branch 47 → 48 taken 437 times.
✓ Branch 47 → 57 taken 4 times.
441 if (ok && !cardColumns.contains(QLatin1String("photo_uri")))
305
1/1
✓ Branch 52 → 53 taken 437 times.
874 ok = addDerivedColumn(QStringLiteral("photo_uri"), [](const ContactCard &card) {
306 ✗ return card.photo.kind == ContactPhoto::Kind::Uri ? card.photo.uri : QString();
307 });
308
5/6
✓ Branch 57 → 58 taken 441 times.
✗ Branch 57 → 62 not taken.
✓ Branch 60 → 61 taken 437 times.
✓ Branch 60 → 62 taken 4 times.
✓ Branch 63 → 64 taken 437 times.
✓ Branch 63 → 71 taken 4 times.
441 if (ok && !cardColumns.contains(QLatin1String("photo_fetched_uri")))
309
1/1
✓ Branch 67 → 68 taken 437 times.
437 ok = exec(m_db, QStringLiteral("ALTER TABLE cards ADD COLUMN photo_fetched_uri TEXT NOT NULL DEFAULT ''"));
310 // v5 (F100-10): the list's optional columns phone, job title and city.
311
5/6
✓ Branch 71 → 72 taken 441 times.
✗ Branch 71 → 76 not taken.
✓ Branch 74 → 75 taken 437 times.
✓ Branch 74 → 76 taken 4 times.
✓ Branch 77 → 78 taken 437 times.
✓ Branch 77 → 87 taken 4 times.
441 if (ok && !cardColumns.contains(QLatin1String("phone")))
312
1/1
✓ Branch 82 → 83 taken 437 times.
874 ok = addDerivedColumn(QStringLiteral("phone"), [](const ContactCard &card) { return ContactIndex::primaryPhone(card); });
313
5/6
✓ Branch 87 → 88 taken 441 times.
✗ Branch 87 → 92 not taken.
✓ Branch 90 → 91 taken 437 times.
✓ Branch 90 → 92 taken 4 times.
✓ Branch 93 → 94 taken 437 times.
✓ Branch 93 → 103 taken 4 times.
441 if (ok && !cardColumns.contains(QLatin1String("job_title")))
314
1/1
✓ Branch 98 → 99 taken 437 times.
874 ok = addDerivedColumn(QStringLiteral("job_title"), [](const ContactCard &card) { return card.title.trimmed(); });
315
5/6
✓ Branch 103 → 104 taken 441 times.
✗ Branch 103 → 108 not taken.
✓ Branch 106 → 107 taken 437 times.
✓ Branch 106 → 108 taken 4 times.
✓ Branch 109 → 110 taken 437 times.
✓ Branch 109 → 119 taken 4 times.
441 if (ok && !cardColumns.contains(QLatin1String("locality")))
316
1/1
✓ Branch 114 → 115 taken 437 times.
874 ok = addDerivedColumn(QStringLiteral("locality"), [](const ContactCard &card) { return ContactIndex::primaryLocality(card); });
317 // v8 (Sprint 107 D-22): `contact_outbox.base_raw` keeps the last server
318 // version of a moved card's source; the merge of a source changed on the
319 // server needs it after the source row is gone.
320
9/19
✓ Branch 119 → 120 taken 441 times.
✗ Branch 119 → 128 not taken.
✓ Branch 123 → 124 taken 441 times.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 126 → 128 taken 440 times.
✓ Branch 129 → 130 taken 441 times.
✗ Branch 129 → 131 not taken.
✓ Branch 131 → 132 taken 441 times.
✗ Branch 131 → 133 not taken.
✓ Branch 133 → 134 taken 441 times.
✗ Branch 133 → 135 not taken.
✓ Branch 135 → 136 taken 1 time.
✓ Branch 135 → 143 taken 440 times.
✗ Branch 238 → 239 not taken.
✗ Branch 238 → 240 not taken.
✗ Branch 242 → 243 not taken.
✗ Branch 242 → 244 not taken.
✗ Branch 246 → 247 not taken.
✗ Branch 246 → 248 not taken.
882 if (ok && !columns(QStringLiteral("contact_outbox")).contains(QLatin1String("base_raw")))
321
1/1
✓ Branch 139 → 140 taken 1 time.
1 ok = exec(m_db, QStringLiteral("ALTER TABLE contact_outbox ADD COLUMN base_raw TEXT NOT NULL DEFAULT ''"));
322 // v7: `anniversary` (as `birthday`) for the wedding year on the relationship graph's partner lines.
323
5/6
✓ Branch 143 → 144 taken 441 times.
✗ Branch 143 → 148 not taken.
✓ Branch 146 → 147 taken 438 times.
✓ Branch 146 → 148 taken 3 times.
✓ Branch 149 → 150 taken 438 times.
✓ Branch 149 → 159 taken 3 times.
441 if (ok && !cardColumns.contains(QLatin1String("anniversary")))
324
1/1
✓ Branch 154 → 155 taken 438 times.
876 ok = addDerivedColumn(QStringLiteral("anniversary"), [](const ContactCard &card) {
325
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1 time.
2 return card.anniversary.isValid() ? card.anniversary.toIso() : QString();
326 });
327 441 return ok;
328 441 }
329
330 2623 bool ContactStore::addDerivedColumn(const QString &column, const std::function<QString(const ContactCard &)> &value) {
331
3/4
✓ Branch 6 → 7 taken 2623 times.
✓ Branch 7 → 8 taken 2623 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2623 times.
5246 if (!exec(m_db, QStringLiteral("ALTER TABLE cards ADD COLUMN %1 TEXT NOT NULL DEFAULT ''").arg(column)))
332 ✗ return false;
333
1/1
✓ Branch 13 → 14 taken 2623 times.
2623 QSqlQuery all(m_db);
334 2623 QList<QPair<qint64, QString>> rows;
335
2/3
✓ Branch 17 → 18 taken 2623 times.
✓ Branch 20 → 21 taken 2623 times.
✗ Branch 20 → 35 not taken.
2623 if (all.exec(QStringLiteral("SELECT id, raw FROM cards")))
336
3/3
✓ Branch 33 → 34 taken 2625 times.
✓ Branch 34 → 22 taken 2 times.
✓ Branch 34 → 35 taken 2623 times.
2625 while (all.next())
337
5/5
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
2 rows.append({all.value(0).toLongLong(), all.value(1).toString()});
338
1/1
✓ Branch 35 → 36 taken 2623 times.
2623 all.finish();
339 2623 bool ok = true;
340
4/4
✓ Branch 36 → 37 taken 2623 times.
✓ Branch 37 → 38 taken 2623 times.
✓ Branch 84 → 39 taken 2 times.
✓ Branch 84 → 85 taken 2623 times.
2625 for (const auto &row : rows) {
341
1/1
✓ Branch 40 → 41 taken 2 times.
2 ContactCard card;
342
3/4
✓ Branch 41 → 42 taken 2 times.
✓ Branch 42 → 43 taken 2 times.
✗ Branch 42 → 44 not taken.
✓ Branch 43 → 45 taken 2 times.
2 const QString derived = VCardCodec::parse(row.second, &card) ? value(card) : QString();
343
2/2
✓ Branch 46 → 47 taken 1 time.
✓ Branch 46 → 48 taken 1 time.
2 if (derived.isEmpty())
344 1 continue;
345
1/1
✓ Branch 48 → 49 taken 1 time.
1 QSqlQuery update(m_db);
346
2/2
✓ Branch 53 → 54 taken 1 time.
✓ Branch 54 → 55 taken 1 time.
2 update.prepare(QStringLiteral("UPDATE cards SET %1 = ? WHERE id = ?").arg(column));
347
1/1
✓ Branch 60 → 61 taken 1 time.
1 update.addBindValue(derived);
348
1/1
✓ Branch 64 → 65 taken 1 time.
1 update.addBindValue(row.first);
349
3/5
✓ Branch 66 → 67 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
✗ Branch 67 → 70 not taken.
✓ Branch 68 → 69 taken 1 time.
✗ Branch 68 → 70 not taken.
1 ok = update.exec() && ok;
350 3 }
351 2623 return ok;
352 2623 }
353
354 // D-5: see the class comment. Runs inside createSchema()'s transaction.
355 7 bool ContactStore::convertLegacyContacts() {
356 7 QSet<QString> columns;
357 {
358
1/1
✓ Branch 3 → 4 taken 7 times.
7 QSqlQuery info(m_db);
359
1/1
✓ Branch 7 → 8 taken 7 times.
7 info.exec(QStringLiteral("PRAGMA table_info(contacts)"));
360
3/3
✓ Branch 17 → 18 taken 101 times.
✓ Branch 18 → 11 taken 94 times.
✓ Branch 18 → 19 taken 7 times.
101 while (info.next())
361
3/3
✓ Branch 11 → 12 taken 94 times.
✓ Branch 12 → 13 taken 94 times.
✓ Branch 13 → 14 taken 94 times.
94 columns.insert(info.value(1).toString());
362 7 }
363
2/2
✓ Branch 39 → 22 taken 28 times.
✓ Branch 39 → 40 taken 7 times.
35 for (const char *column : {"carddav_href", "carddav_vuid", "carddav_account_id", "carddav_book_path"})
364
3/3
✓ Branch 23 → 24 taken 28 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 26 → 38 taken 24 times.
28 if (!columns.contains(QLatin1String(column)))
365
2/2
✓ Branch 32 → 33 taken 4 times.
✓ Branch 33 → 34 taken 4 times.
8 exec(m_db, QStringLiteral("ALTER TABLE contacts ADD COLUMN %1 TEXT DEFAULT ''").arg(QLatin1String(column)));
366
1/1
✓ Branch 43 → 44 taken 7 times.
7 bool ok = exec(m_db, QStringLiteral(
367 "INSERT OR REPLACE INTO address_usage (address_key, use_count, last_used) "
368 "SELECT lower(trim(email)), SUM(use_count), COALESCE(MAX(last_used), '') FROM contacts "
369 "WHERE use_count > 0 AND trim(email) <> '' GROUP BY lower(trim(email))"));
370 // Server rows: one read-only legacy card per resource of each book.
371 struct LegacyCard {
372 qint64 bookId = 0;
373 QString uid;
374 QString href;
375 ContactCard card;
376 };
377 7 QHash<QString, LegacyCard> legacy;
378 7 QStringList order;
379
1/1
✓ Branch 46 → 47 taken 7 times.
7 QSqlQuery rows(m_db);
380
1/1
✓ Branch 50 → 51 taken 7 times.
7 rows.exec(QStringLiteral(
381 "SELECT id, display_name, email, carddav_uid, carddav_href, carddav_vuid, carddav_account_id, "
382 "carddav_book_path FROM contacts WHERE source = 'carddav' ORDER BY id"));
383
3/3
✓ Branch 149 → 150 taken 17 times.
✓ Branch 150 → 54 taken 10 times.
✓ Branch 150 → 151 taken 7 times.
17 while (rows.next()) {
384
2/2
✓ Branch 54 → 55 taken 10 times.
✓ Branch 55 → 56 taken 10 times.
10 const QString account = rows.value(6).toString();
385
2/2
✓ Branch 57 → 58 taken 10 times.
✓ Branch 58 → 59 taken 10 times.
10 const QString path = rows.value(7).toString();
386
8/15
✓ Branch 61 → 62 taken 8 times.
✓ Branch 61 → 64 taken 2 times.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 70 taken 8 times.
✓ Branch 68 → 69 taken 2 times.
✓ Branch 74 → 75 taken 2 times.
✓ Branch 74 → 76 taken 8 times.
✓ Branch 76 → 77 taken 2 times.
✓ Branch 76 → 78 taken 8 times.
✗ Branch 427 → 428 not taken.
✗ Branch 427 → 429 not taken.
✗ Branch 431 → 432 not taken.
✗ Branch 431 → 433 not taken.
✗ Branch 435 → 436 not taken.
✗ Branch 435 → 437 not taken.
22 const qint64 bookId = account.isEmpty() || path.isEmpty() ? ensureBook(QString(), QStringLiteral("legacy"))
387
3/3
✓ Branch 70 → 71 taken 8 times.
✓ Branch 72 → 73 taken 2 times.
✓ Branch 72 → 74 taken 8 times.
10 : ensureBook(account, path);
388
2/2
✓ Branch 78 → 79 taken 10 times.
✓ Branch 79 → 80 taken 10 times.
10 QString uid = rows.value(5).toString();
389
2/2
✓ Branch 82 → 83 taken 2 times.
✓ Branch 82 → 89 taken 8 times.
10 if (uid.isEmpty())
390
2/2
✓ Branch 83 → 84 taken 2 times.
✓ Branch 84 → 85 taken 2 times.
2 uid = rows.value(3).toString();
391
1/2
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 104 taken 10 times.
10 if (uid.isEmpty())
392 ✗ uid = QStringLiteral("legacy-%1").arg(rows.value(0).toLongLong());
393
2/2
✓ Branch 104 → 105 taken 10 times.
✓ Branch 105 → 106 taken 10 times.
10 const QString href = rows.value(4).toString();
394
5/5
✓ Branch 108 → 109 taken 2 times.
✓ Branch 108 → 110 taken 8 times.
✓ Branch 113 → 114 taken 10 times.
✓ Branch 114 → 115 taken 10 times.
✓ Branch 115 → 116 taken 10 times.
10 const QString key = QString::number(bookId) + QLatin1Char('\n') + (href.isEmpty() ? uid : href);
395
2/2
✓ Branch 119 → 120 taken 8 times.
✓ Branch 119 → 133 taken 2 times.
10 if (!legacy.contains(key)) {
396
1/1
✓ Branch 120 → 121 taken 8 times.
8 LegacyCard entry;
397 8 entry.bookId = bookId;
398 8 entry.uid = uid;
399 8 entry.href = href;
400 8 entry.card.uid = uid;
401
2/2
✓ Branch 124 → 125 taken 8 times.
✓ Branch 125 → 126 taken 8 times.
8 entry.card.formattedName = rows.value(1).toString();
402
1/1
✓ Branch 129 → 130 taken 8 times.
8 legacy.insert(key, entry);
403
1/1
✓ Branch 130 → 131 taken 8 times.
8 order.append(key);
404 8 }
405 10 ContactValue address;
406
2/2
✓ Branch 133 → 134 taken 10 times.
✓ Branch 134 → 135 taken 10 times.
10 address.value = rows.value(2).toString();
407
1/2
✓ Branch 139 → 140 taken 10 times.
✗ Branch 139 → 142 not taken.
10 if (!address.value.isEmpty())
408
2/2
✓ Branch 140 → 141 taken 10 times.
✓ Branch 141 → 142 taken 10 times.
10 legacy[key].card.emails.append(address);
409 10 }
410
4/4
✓ Branch 151 → 152 taken 7 times.
✓ Branch 152 → 153 taken 7 times.
✓ Branch 183 → 154 taken 8 times.
✓ Branch 183 → 184 taken 7 times.
15 for (const QString &key : order) {
411 8 const LegacyCard &entry = legacy.value(key);
412 8 StoredCard card;
413 8 card.bookId = entry.bookId;
414 8 card.uid = entry.uid;
415 8 card.href = entry.href;
416
1/1
✓ Branch 162 → 163 taken 8 times.
16 card.raw = VCardCodec::create(entry.card);
417 8 card.syncState = QStringLiteral("legacy");
418 8 card.writable = false;
419
3/5
✓ Branch 174 → 175 taken 8 times.
✓ Branch 175 → 176 taken 8 times.
✗ Branch 175 → 178 not taken.
✓ Branch 176 → 177 taken 8 times.
✗ Branch 176 → 178 not taken.
8 ok = putCard(card) > 0 && ok;
420 8 }
421 // Local rows: collected (used when sending) or the user's own contacts.
422 7 QSet<QString> serverAddresses;
423 {
424
1/1
✓ Branch 185 → 186 taken 7 times.
7 QSqlQuery addresses(m_db);
425
1/1
✓ Branch 189 → 190 taken 7 times.
7 addresses.exec(QStringLiteral("SELECT DISTINCT lower(trim(email)) FROM contacts WHERE source = 'carddav'"));
426
3/3
✓ Branch 199 → 200 taken 17 times.
✓ Branch 200 → 193 taken 10 times.
✓ Branch 200 → 201 taken 7 times.
17 while (addresses.next())
427
3/3
✓ Branch 193 → 194 taken 10 times.
✓ Branch 194 → 195 taken 10 times.
✓ Branch 195 → 196 taken 10 times.
10 serverAddresses.insert(addresses.value(0).toString());
428 7 }
429 7 QStringList pending;
430
1/1
✓ Branch 202 → 203 taken 7 times.
7 QSqlQuery locals(m_db);
431
1/1
✓ Branch 206 → 207 taken 7 times.
7 locals.exec(QStringLiteral("SELECT display_name, email, use_count, created_at, last_used FROM contacts "
432 "WHERE source = 'local' AND trim(email) <> '' ORDER BY id"));
433
3/3
✓ Branch 332 → 333 taken 14 times.
✓ Branch 333 → 210 taken 7 times.
✓ Branch 333 → 334 taken 7 times.
14 while (locals.next()) {
434
3/3
✓ Branch 210 → 211 taken 7 times.
✓ Branch 211 → 212 taken 7 times.
✓ Branch 212 → 213 taken 7 times.
7 const QString address = locals.value(1).toString().trimmed();
435
1/1
✓ Branch 215 → 216 taken 7 times.
7 const QString key = ContactIndex::addressKey(address);
436
4/4
✓ Branch 216 → 217 taken 7 times.
✓ Branch 217 → 218 taken 7 times.
✓ Branch 219 → 220 taken 3 times.
✓ Branch 219 → 288 taken 4 times.
7 if (locals.value(2).toInt() > 0) {
437
1/2
✗ Branch 221 → 222 not taken.
✓ Branch 221 → 223 taken 3 times.
3 if (serverAddresses.contains(key))
438 ✗ continue; // the usage count is enough
439
1/1
✓ Branch 223 → 224 taken 3 times.
3 QSqlQuery insert(m_db);
440
1/1
✓ Branch 227 → 228 taken 3 times.
3 insert.prepare(QStringLiteral("INSERT OR IGNORE INTO collected_addresses "
441 "(address_key, address, display_name, first_used, last_used) VALUES (?, ?, ?, ?, ?)"));
442
2/2
✓ Branch 232 → 233 taken 3 times.
✓ Branch 233 → 234 taken 3 times.
3 insert.addBindValue(sqlValue(key));
443
2/2
✓ Branch 238 → 239 taken 3 times.
✓ Branch 239 → 240 taken 3 times.
3 insert.addBindValue(sqlValue(address));
444
8/12
✓ Branch 243 → 244 taken 3 times.
✓ Branch 244 → 245 taken 3 times.
✗ Branch 246 → 247 not taken.
✓ Branch 246 → 248 taken 3 times.
✓ Branch 248 → 249 taken 3 times.
✓ Branch 249 → 250 taken 3 times.
✓ Branch 251 → 252 taken 3 times.
✓ Branch 252 → 253 taken 3 times.
✓ Branch 256 → 257 taken 3 times.
✗ Branch 256 → 258 not taken.
✗ Branch 565 → 566 not taken.
✗ Branch 565 → 567 not taken.
3 insert.addBindValue(sqlValue(locals.value(0).toString() == address ? QString() : locals.value(0).toString()));
445
4/4
✓ Branch 261 → 262 taken 3 times.
✓ Branch 262 → 263 taken 3 times.
✓ Branch 264 → 265 taken 3 times.
✓ Branch 265 → 266 taken 3 times.
3 insert.addBindValue(sqlValue(locals.value(3).toString()));
446
4/4
✓ Branch 271 → 272 taken 3 times.
✓ Branch 272 → 273 taken 3 times.
✓ Branch 274 → 275 taken 3 times.
✓ Branch 275 → 276 taken 3 times.
3 insert.addBindValue(sqlValue(locals.value(4).toString()));
447
3/5
✓ Branch 280 → 281 taken 3 times.
✓ Branch 281 → 282 taken 3 times.
✗ Branch 281 → 284 not taken.
✓ Branch 282 → 283 taken 3 times.
✗ Branch 282 → 284 not taken.
3 ok = insert.exec() && ok;
448 3 continue;
449 3 }
450
1/1
✓ Branch 288 → 289 taken 4 times.
4 ContactCard card;
451
2/2
✓ Branch 289 → 290 taken 4 times.
✓ Branch 290 → 291 taken 4 times.
4 card.uid = QUuid::createUuid().toString(QUuid::WithoutBraces);
452
6/10
✓ Branch 293 → 294 taken 4 times.
✓ Branch 294 → 295 taken 4 times.
✗ Branch 296 → 297 not taken.
✓ Branch 296 → 298 taken 4 times.
✓ Branch 298 → 299 taken 4 times.
✓ Branch 299 → 300 taken 4 times.
✓ Branch 302 → 303 taken 4 times.
✗ Branch 302 → 304 not taken.
✗ Branch 610 → 611 not taken.
✗ Branch 610 → 612 not taken.
4 card.formattedName = locals.value(0).toString() == address ? QString() : locals.value(0).toString();
453 4 ContactValue value;
454 4 value.value = address;
455
2/4
✓ Branch 310 → 311 taken 4 times.
✓ Branch 310 → 312 taken 4 times.
✗ Branch 622 → 623 not taken.
✗ Branch 622 → 624 not taken.
8 card.emails = {value};
456
1/1
✓ Branch 312 → 313 taken 4 times.
4 const qint64 id = addLocalCard(card);
457
2/4
✓ Branch 313 → 314 taken 4 times.
✗ Branch 313 → 316 not taken.
✓ Branch 314 → 315 taken 4 times.
✗ Branch 314 → 316 not taken.
4 ok = id > 0 && ok;
458
2/2
✓ Branch 317 → 318 taken 4 times.
✓ Branch 318 → 319 taken 4 times.
4 pending << QString::number(id);
459 10 }
460
2/2
✓ Branch 336 → 337 taken 7 times.
✓ Branch 340 → 341 taken 7 times.
14 setMeta(QStringLiteral("legacyLocalPending"), pending.join(QLatin1Char(',')));
461
1/1
✓ Branch 350 → 351 taken 7 times.
14 setMeta(QStringLiteral("legacyTable"), QStringLiteral("present"));
462
8/8
✓ Branch 355 → 356 taken 7 times.
✓ Branch 359 → 360 taken 7 times.
✓ Branch 360 → 361 taken 7 times.
✓ Branch 362 → 363 taken 7 times.
✓ Branch 363 → 364 taken 7 times.
✓ Branch 365 → 366 taken 7 times.
✓ Branch 369 → 357 taken 7 times.
✓ Branch 369 → 370 taken 7 times.
14 qCInfo(lcContactStore) << "Contact schema v1: legacy cards" << order.size() << "local-only cards" << pending.size();
463 7 return ok;
464 11 }
465
466 // ─── address books ─────────────────────────────────────────
467
468 17117 qint64 ContactStore::ensureBook(const QString &accountId, const QString &path) {
469
3/3
✓ Branch 2 → 3 taken 17117 times.
✓ Branch 4 → 5 taken 16478 times.
✓ Branch 4 → 7 taken 639 times.
17117 if (const auto existing = book(accountId, path))
470
2/2
✓ Branch 9 → 10 taken 639 times.
✓ Branch 9 → 12 taken 16478 times.
17117 return existing->id;
471
1/1
✓ Branch 11 → 13 taken 639 times.
639 QSqlQuery q(m_db);
472
1/1
✓ Branch 16 → 17 taken 639 times.
639 q.prepare(QStringLiteral("INSERT INTO address_books (account_id, path) VALUES (?, ?)"));
473
2/2
✓ Branch 21 → 22 taken 639 times.
✓ Branch 22 → 23 taken 639 times.
639 q.addBindValue(sqlValue(accountId));
474
2/2
✓ Branch 27 → 28 taken 639 times.
✓ Branch 28 → 29 taken 639 times.
639 q.addBindValue(sqlValue(path));
475
3/3
✓ Branch 31 → 32 taken 639 times.
✓ Branch 32 → 33 taken 7 times.
✓ Branch 32 → 49 taken 632 times.
639 if (!q.exec()) {
476
8/8
✓ Branch 33 → 34 taken 7 times.
✓ Branch 37 → 38 taken 7 times.
✓ Branch 38 → 39 taken 7 times.
✓ Branch 39 → 40 taken 7 times.
✓ Branch 40 → 41 taken 7 times.
✓ Branch 41 → 42 taken 7 times.
✓ Branch 47 → 35 taken 7 times.
✓ Branch 47 → 48 taken 7 times.
14 qCWarning(lcContactStore) << "ensureBook failed:" << q.lastError().text();
477 7 return 0;
478 }
479
2/2
✓ Branch 49 → 50 taken 632 times.
✓ Branch 50 → 51 taken 632 times.
632 return q.lastInsertId().toLongLong();
480 639 }
481
482
1/1
✓ Branch 6 → 7 taken 16655 times.
33310 qint64 ContactStore::localBookId() { return ensureBook(QString(), QStringLiteral("local")); }
483
484 5081 QList<AddressBookInfo> ContactStore::books() const {
485 5081 QList<AddressBookInfo> result;
486
1/1
✓ Branch 2 → 3 taken 5081 times.
5081 QSqlQuery q(m_db);
487
1/1
✓ Branch 9 → 10 taken 5081 times.
15243 q.exec(QStringLiteral("SELECT %1 FROM address_books ORDER BY account_id, display_name, path")
488
1/1
✓ Branch 8 → 9 taken 5081 times.
10162 .arg(QLatin1String(kBookColumns)));
489
3/3
✓ Branch 18 → 19 taken 11109 times.
✓ Branch 19 → 14 taken 6028 times.
✓ Branch 19 → 20 taken 5081 times.
11109 while (q.next())
490
2/2
✓ Branch 14 → 15 taken 6028 times.
✓ Branch 15 → 16 taken 6028 times.
6028 result.append(bookFrom(q));
491 5081 return result;
492 5081 }
493
494 3304 std::optional<AddressBookInfo> ContactStore::book(qint64 id) const {
495
1/1
✓ Branch 2 → 3 taken 3304 times.
3304 QSqlQuery q(m_db);
496
2/2
✓ Branch 8 → 9 taken 3304 times.
✓ Branch 9 → 10 taken 3304 times.
6608 q.prepare(QStringLiteral("SELECT %1 FROM address_books WHERE id = ?").arg(QLatin1String(kBookColumns)));
497
2/2
✓ Branch 15 → 16 taken 3304 times.
✓ Branch 16 → 17 taken 3304 times.
3304 q.addBindValue(sqlValue(id));
498
7/8
✓ Branch 19 → 20 taken 3304 times.
✓ Branch 20 → 21 taken 3304 times.
✗ Branch 20 → 24 not taken.
✓ Branch 21 → 22 taken 3304 times.
✓ Branch 22 → 23 taken 2450 times.
✓ Branch 22 → 24 taken 854 times.
✓ Branch 25 → 26 taken 2450 times.
✓ Branch 25 → 30 taken 854 times.
3304 if (q.exec() && q.next())
499
1/1
✓ Branch 26 → 27 taken 2450 times.
2450 return bookFrom(q);
500 854 return std::nullopt;
501 3304 }
502
503 20118 std::optional<AddressBookInfo> ContactStore::book(const QString &accountId, const QString &path) const {
504
1/1
✓ Branch 2 → 3 taken 20118 times.
20118 QSqlQuery q(m_db);
505
1/1
✓ Branch 9 → 10 taken 20118 times.
60354 q.prepare(QStringLiteral("SELECT %1 FROM address_books WHERE account_id = ? AND path = ?")
506
1/1
✓ Branch 8 → 9 taken 20118 times.
60354 .arg(QLatin1String(kBookColumns)));
507
2/2
✓ Branch 15 → 16 taken 20118 times.
✓ Branch 16 → 17 taken 20118 times.
20118 q.addBindValue(sqlValue(accountId));
508
2/2
✓ Branch 21 → 22 taken 20118 times.
✓ Branch 22 → 23 taken 20118 times.
20118 q.addBindValue(sqlValue(path));
509
8/8
✓ Branch 25 → 26 taken 20118 times.
✓ Branch 26 → 27 taken 20111 times.
✓ Branch 26 → 30 taken 7 times.
✓ Branch 27 → 28 taken 20111 times.
✓ Branch 28 → 29 taken 16955 times.
✓ Branch 28 → 30 taken 3156 times.
✓ Branch 31 → 32 taken 16955 times.
✓ Branch 31 → 36 taken 3163 times.
20118 if (q.exec() && q.next())
510
1/1
✓ Branch 32 → 33 taken 16955 times.
16955 return bookFrom(q);
511 3163 return std::nullopt;
512 20118 }
513
514 368 bool ContactStore::updateBook(const AddressBookInfo &book) {
515
1/1
✓ Branch 2 → 3 taken 368 times.
368 QSqlQuery q(m_db);
516
1/1
✓ Branch 6 → 7 taken 368 times.
368 q.prepare(QStringLiteral(
517 "UPDATE address_books SET display_name = ?, read_only = ?, ctag = ?, sync_token = ?, "
518 "supports_sync_collection = ?, supported_versions = ?, max_resource_size = ?, fetched = ?, "
519 "last_sync = ?, last_error = ?, skipped_cards = ? WHERE id = ?"));
520
2/2
✓ Branch 11 → 12 taken 368 times.
✓ Branch 12 → 13 taken 368 times.
368 q.addBindValue(sqlValue(book.displayName));
521
2/2
✓ Branch 17 → 18 taken 368 times.
✓ Branch 18 → 19 taken 368 times.
368 q.addBindValue(sqlValue(book.readOnly));
522
2/2
✓ Branch 23 → 24 taken 368 times.
✓ Branch 24 → 25 taken 368 times.
368 q.addBindValue(sqlValue(book.ctag));
523
2/2
✓ Branch 29 → 30 taken 368 times.
✓ Branch 30 → 31 taken 368 times.
368 q.addBindValue(sqlValue(book.syncToken));
524
2/2
✓ Branch 35 → 36 taken 368 times.
✓ Branch 36 → 37 taken 368 times.
368 q.addBindValue(sqlValue(book.supportsSyncCollection));
525
3/3
✓ Branch 42 → 43 taken 368 times.
✓ Branch 44 → 45 taken 368 times.
✓ Branch 45 → 46 taken 368 times.
368 q.addBindValue(sqlValue(book.supportedVersions.join(QLatin1Char(','))));
526
2/2
✓ Branch 51 → 52 taken 368 times.
✓ Branch 52 → 53 taken 368 times.
368 q.addBindValue(sqlValue(book.maxResourceSize));
527
2/2
✓ Branch 57 → 58 taken 368 times.
✓ Branch 58 → 59 taken 368 times.
368 q.addBindValue(sqlValue(book.fetched));
528
6/6
✓ Branch 62 → 63 taken 368 times.
✓ Branch 63 → 64 taken 199 times.
✓ Branch 63 → 65 taken 169 times.
✓ Branch 64 → 66 taken 199 times.
✓ Branch 67 → 68 taken 368 times.
✓ Branch 68 → 69 taken 368 times.
368 q.addBindValue(sqlValue(book.lastSync.isValid() ? book.lastSync.toString(Qt::ISODate) : QString()));
529
2/2
✓ Branch 74 → 75 taken 368 times.
✓ Branch 75 → 76 taken 368 times.
368 q.addBindValue(sqlValue(book.lastError));
530
2/2
✓ Branch 80 → 81 taken 368 times.
✓ Branch 81 → 82 taken 368 times.
368 q.addBindValue(sqlValue(book.skippedCards));
531
2/2
✓ Branch 86 → 87 taken 368 times.
✓ Branch 87 → 88 taken 368 times.
368 q.addBindValue(sqlValue(book.id));
532
1/1
✓ Branch 90 → 91 taken 368 times.
736 return q.exec();
533 368 }
534
535 4 bool ContactStore::setBookFlags(qint64 id, bool showInAll, bool useForCompletion) {
536
1/1
✓ Branch 2 → 3 taken 4 times.
4 QSqlQuery q(m_db);
537
1/1
✓ Branch 6 → 7 taken 4 times.
4 q.prepare(QStringLiteral("UPDATE address_books SET show_in_all = ?, use_for_completion = ? WHERE id = ?"));
538
2/2
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
4 q.addBindValue(sqlValue(showInAll));
539
2/2
✓ Branch 17 → 18 taken 4 times.
✓ Branch 18 → 19 taken 4 times.
4 q.addBindValue(sqlValue(useForCompletion));
540
2/2
✓ Branch 23 → 24 taken 4 times.
✓ Branch 24 → 25 taken 4 times.
4 q.addBindValue(sqlValue(id));
541
5/6
✓ Branch 27 → 28 taken 4 times.
✓ Branch 28 → 29 taken 4 times.
✗ Branch 28 → 32 not taken.
✓ Branch 29 → 30 taken 4 times.
✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 32 taken 1 time.
8 return q.exec() && q.numRowsAffected() == 1; // an unknown book is no success
542 4 }
543
544 15 bool ContactStore::removeBook(qint64 id) {
545
1/1
✓ Branch 3 → 4 taken 15 times.
30 return inSavepoint([&] {
546
1/1
✓ Branch 2 → 3 taken 15 times.
15 QSqlQuery q(m_db);
547
1/1
✓ Branch 6 → 7 taken 15 times.
15 q.prepare(QStringLiteral("DELETE FROM address_books WHERE id = ?"));
548
2/2
✓ Branch 11 → 12 taken 15 times.
✓ Branch 12 → 13 taken 15 times.
15 q.addBindValue(sqlValue(id));
549
1/1
✓ Branch 15 → 16 taken 15 times.
15 QSqlQuery outbox(m_db);
550
1/1
✓ Branch 19 → 20 taken 15 times.
15 outbox.prepare(QStringLiteral("DELETE FROM contact_outbox WHERE book_id = ?"));
551
2/2
✓ Branch 24 → 25 taken 15 times.
✓ Branch 25 → 26 taken 15 times.
15 outbox.addBindValue(sqlValue(id));
552
5/6
✓ Branch 28 → 29 taken 15 times.
✓ Branch 29 → 30 taken 15 times.
✗ Branch 29 → 33 not taken.
✓ Branch 30 → 31 taken 15 times.
✓ Branch 31 → 32 taken 14 times.
✓ Branch 31 → 33 taken 1 time.
30 return q.exec() && outbox.exec();
553 45 });
554 }
555
556 // ─── cards ─────────────────────────────────────────────────
557
558 2888 std::optional<StoredCard> ContactStore::card(qint64 bookId, const QString &uid) const {
559
1/1
✓ Branch 2 → 3 taken 2888 times.
2888 QSqlQuery q(m_db);
560
2/2
✓ Branch 8 → 9 taken 2888 times.
✓ Branch 9 → 10 taken 2888 times.
5776 q.prepare(QStringLiteral("SELECT %1 FROM cards WHERE book_id = ? AND uid = ?").arg(QLatin1String(kCardColumns)));
561
2/2
✓ Branch 15 → 16 taken 2888 times.
✓ Branch 16 → 17 taken 2888 times.
2888 q.addBindValue(sqlValue(bookId));
562
2/2
✓ Branch 21 → 22 taken 2888 times.
✓ Branch 22 → 23 taken 2888 times.
2888 q.addBindValue(sqlValue(uid));
563
8/8
✓ Branch 25 → 26 taken 2888 times.
✓ Branch 26 → 27 taken 2887 times.
✓ Branch 26 → 30 taken 1 time.
✓ Branch 27 → 28 taken 2887 times.
✓ Branch 28 → 29 taken 2767 times.
✓ Branch 28 → 30 taken 120 times.
✓ Branch 31 → 32 taken 2767 times.
✓ Branch 31 → 36 taken 121 times.
2888 if (q.exec() && q.next())
564
1/1
✓ Branch 32 → 33 taken 2767 times.
2767 return cardFrom(q);
565 121 return std::nullopt;
566 2888 }
567
568 4 QList<StoredCard> ContactStore::cards(qint64 bookId) const {
569 4 QList<StoredCard> result;
570
1/1
✓ Branch 2 → 3 taken 4 times.
4 QSqlQuery q(m_db);
571
2/2
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
8 q.prepare(QStringLiteral("SELECT %1 FROM cards WHERE book_id = ? ORDER BY id").arg(QLatin1String(kCardColumns)));
572
1/1
✓ Branch 15 → 16 taken 4 times.
4 q.addBindValue(bookId);
573
2/3
✓ Branch 17 → 18 taken 4 times.
✓ Branch 18 → 19 taken 4 times.
✗ Branch 18 → 26 not taken.
4 if (q.exec())
574
3/3
✓ Branch 24 → 25 taken 11 times.
✓ Branch 25 → 20 taken 7 times.
✓ Branch 25 → 26 taken 4 times.
11 while (q.next())
575
2/2
✓ Branch 20 → 21 taken 7 times.
✓ Branch 21 → 22 taken 7 times.
7 result.append(cardFrom(q));
576 4 return result;
577 4 }
578
579 8 std::optional<StoredCard> ContactStore::cardById(qint64 id) const {
580
1/1
✓ Branch 2 → 3 taken 8 times.
8 QSqlQuery q(m_db);
581
2/2
✓ Branch 8 → 9 taken 8 times.
✓ Branch 9 → 10 taken 8 times.
16 q.prepare(QStringLiteral("SELECT %1 FROM cards WHERE id = ?").arg(QLatin1String(kCardColumns)));
582
2/2
✓ Branch 15 → 16 taken 8 times.
✓ Branch 16 → 17 taken 8 times.
8 q.addBindValue(sqlValue(id));
583
7/8
✓ Branch 19 → 20 taken 8 times.
✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 24 taken 1 time.
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
✗ Branch 22 → 24 not taken.
✓ Branch 25 → 26 taken 7 times.
✓ Branch 25 → 30 taken 1 time.
8 if (q.exec() && q.next())
584
1/1
✓ Branch 26 → 27 taken 7 times.
7 return cardFrom(q);
585 1 return std::nullopt;
586 8 }
587
588 5308 std::optional<StoredCard> ContactStore::cardByHref(qint64 bookId, const QString &href) const {
589
1/1
✓ Branch 2 → 3 taken 5308 times.
5308 QSqlQuery q(m_db);
590
1/1
✓ Branch 9 → 10 taken 5308 times.
15924 q.prepare(QStringLiteral("SELECT %1 FROM cards WHERE book_id = ? AND href = ? AND sync_state <> 'legacy'")
591
1/1
✓ Branch 8 → 9 taken 5308 times.
15924 .arg(QLatin1String(kCardColumns)));
592
2/2
✓ Branch 15 → 16 taken 5308 times.
✓ Branch 16 → 17 taken 5308 times.
5308 q.addBindValue(sqlValue(bookId));
593
2/2
✓ Branch 21 → 22 taken 5308 times.
✓ Branch 22 → 23 taken 5308 times.
5308 q.addBindValue(sqlValue(href));
594
7/8
✓ Branch 25 → 26 taken 5308 times.
✓ Branch 26 → 27 taken 5308 times.
✗ Branch 26 → 30 not taken.
✓ Branch 27 → 28 taken 5308 times.
✓ Branch 28 → 29 taken 10 times.
✓ Branch 28 → 30 taken 5298 times.
✓ Branch 31 → 32 taken 10 times.
✓ Branch 31 → 36 taken 5298 times.
5308 if (q.exec() && q.next())
595
1/1
✓ Branch 32 → 33 taken 10 times.
10 return cardFrom(q);
596 5298 return std::nullopt;
597 5308 }
598
599 5308 QString ContactStore::uidForHref(qint64 bookId, const QString &href) const {
600
1/1
✓ Branch 2 → 3 taken 5308 times.
5308 const auto found = cardByHref(bookId, href);
601
2/2
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 7 taken 5298 times.
10616 return found ? found->uid : QString();
602 5308 }
603
604 51 QHash<QString, QString> ContactStore::cardEtags(qint64 bookId) const {
605 51 QHash<QString, QString> result;
606
1/1
✓ Branch 2 → 3 taken 51 times.
51 QSqlQuery q(m_db);
607
1/1
✓ Branch 6 → 7 taken 51 times.
51 q.prepare(QStringLiteral("SELECT href, etag FROM cards WHERE book_id = ? AND href <> '' AND sync_state <> 'legacy'"));
608
2/2
✓ Branch 11 → 12 taken 51 times.
✓ Branch 12 → 13 taken 51 times.
51 q.addBindValue(sqlValue(bookId));
609
3/3
✓ Branch 15 → 16 taken 51 times.
✓ Branch 16 → 17 taken 50 times.
✓ Branch 16 → 30 taken 1 time.
51 if (q.exec())
610
3/3
✓ Branch 28 → 29 taken 346 times.
✓ Branch 29 → 18 taken 296 times.
✓ Branch 29 → 30 taken 50 times.
346 while (q.next())
611
5/5
✓ Branch 18 → 19 taken 296 times.
✓ Branch 19 → 20 taken 296 times.
✓ Branch 20 → 21 taken 296 times.
✓ Branch 21 → 22 taken 296 times.
✓ Branch 22 → 23 taken 296 times.
296 result.insert(q.value(0).toString(), q.value(1).toString());
612 51 return result;
613 51 }
614
615 // A SQLite savepoint nests inside a transaction the caller holds (sync
616 // pages) and is its own transaction otherwise; a failed write rolls back
617 // to it. S106-154/167: the card row and its ~15 derived-index statements,
618 // and a book with its outbox, change together or not at all.
619 30517 bool ContactStore::inSavepoint(const std::function<bool()> &write) {
620
1/1
✓ Branch 2 → 3 taken 30517 times.
30517 QSqlQuery q(m_db);
621
2/3
✓ Branch 6 → 7 taken 30517 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 26 taken 30517 times.
30517 if (!q.exec(QStringLiteral("SAVEPOINT contact_write"))) {
622 ✗ qCWarning(lcContactStore) << "savepoint:" << q.lastError().text();
623 ✗ return false;
624 }
625
11/16
✓ Branch 26 → 27 taken 30517 times.
✓ Branch 27 → 28 taken 30511 times.
✓ Branch 27 → 34 taken 6 times.
✓ Branch 31 → 32 taken 30511 times.
✓ Branch 32 → 33 taken 30511 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 30511 times.
✓ Branch 35 → 37 taken 6 times.
✓ Branch 37 → 38 taken 30511 times.
✓ Branch 37 → 39 taken 6 times.
✓ Branch 39 → 40 taken 30511 times.
✓ Branch 39 → 41 taken 6 times.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 80 not taken.
61028 if (write() && q.exec(QStringLiteral("RELEASE contact_write")))
626 30511 return true;
627
1/1
✓ Branch 44 → 45 taken 6 times.
6 q.exec(QStringLiteral("ROLLBACK TO contact_write"));
628
1/1
✓ Branch 50 → 51 taken 6 times.
6 q.exec(QStringLiteral("RELEASE contact_write"));
629 6 return false;
630 30517 }
631
632 30502 qint64 ContactStore::putCard(const StoredCard &card) {
633 30502 qint64 id = 0;
634
2/2
✓ Branch 2 → 3 taken 30502 times.
✓ Branch 3 → 4 taken 30502 times.
61004 inSavepoint([&] { id = writeCard(card); return id > 0; });
635 30502 return id;
636 }
637
638 30502 qint64 ContactStore::writeCard(const StoredCard &card) {
639
5/6
✓ Branch 2 → 3 taken 30501 times.
✓ Branch 2 → 5 taken 1 time.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 30501 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 30501 times.
30502 if (card.bookId <= 0 || card.uid.isEmpty())
640 1 return 0;
641
1/1
✓ Branch 9 → 10 taken 30501 times.
30501 QSqlQuery update(m_db);
642
1/1
✓ Branch 13 → 14 taken 30501 times.
30501 update.prepare(QStringLiteral(
643 "UPDATE cards SET href = ?, etag = ?, raw = ?, base_raw = ?, sync_state = ?, writable = ?, "
644 "updated_at = datetime('now') WHERE book_id = ? AND uid = ?"));
645
2/2
✓ Branch 18 → 19 taken 30501 times.
✓ Branch 19 → 20 taken 30501 times.
30501 update.addBindValue(sqlValue(card.href));
646
2/2
✓ Branch 24 → 25 taken 30501 times.
✓ Branch 25 → 26 taken 30501 times.
30501 update.addBindValue(sqlValue(card.etag));
647
2/2
✓ Branch 30 → 31 taken 30501 times.
✓ Branch 31 → 32 taken 30501 times.
30501 update.addBindValue(sqlValue(card.raw));
648
2/2
✓ Branch 36 → 37 taken 30501 times.
✓ Branch 37 → 38 taken 30501 times.
30501 update.addBindValue(sqlValue(card.baseRaw));
649
2/2
✓ Branch 42 → 43 taken 30501 times.
✓ Branch 43 → 44 taken 30501 times.
30501 update.addBindValue(sqlValue(card.syncState));
650
2/2
✓ Branch 48 → 49 taken 30501 times.
✓ Branch 49 → 50 taken 30501 times.
30501 update.addBindValue(sqlValue(card.writable));
651
2/2
✓ Branch 54 → 55 taken 30501 times.
✓ Branch 55 → 56 taken 30501 times.
30501 update.addBindValue(sqlValue(card.bookId));
652
2/2
✓ Branch 60 → 61 taken 30501 times.
✓ Branch 61 → 62 taken 30501 times.
30501 update.addBindValue(sqlValue(card.uid));
653
3/3
✓ Branch 64 → 65 taken 30501 times.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 82 taken 30500 times.
30501 if (!update.exec()) {
654
8/8
✓ Branch 66 → 67 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 72 → 73 taken 1 time.
✓ Branch 73 → 74 taken 1 time.
✓ Branch 74 → 75 taken 1 time.
✓ Branch 80 → 68 taken 1 time.
✓ Branch 80 → 81 taken 1 time.
2 qCWarning(lcContactStore) << "putCard failed:" << update.lastError().text();
655 1 return 0;
656 }
657 30500 qint64 id = 0;
658
3/3
✓ Branch 82 → 83 taken 30500 times.
✓ Branch 83 → 84 taken 284 times.
✓ Branch 83 → 90 taken 30216 times.
30500 if (update.numRowsAffected() > 0) {
659
1/1
✓ Branch 84 → 85 taken 284 times.
284 id = this->card(card.bookId, card.uid).value_or(StoredCard{}).id;
660 } else {
661
1/1
✓ Branch 90 → 91 taken 30216 times.
30216 QSqlQuery insert(m_db);
662
1/1
✓ Branch 94 → 95 taken 30216 times.
30216 insert.prepare(QStringLiteral("INSERT INTO cards (book_id, uid, href, etag, raw, base_raw, sync_state, writable) "
663 "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"));
664
2/2
✓ Branch 99 → 100 taken 30216 times.
✓ Branch 100 → 101 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.bookId));
665
2/2
✓ Branch 105 → 106 taken 30216 times.
✓ Branch 106 → 107 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.uid));
666
2/2
✓ Branch 111 → 112 taken 30216 times.
✓ Branch 112 → 113 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.href));
667
2/2
✓ Branch 117 → 118 taken 30216 times.
✓ Branch 118 → 119 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.etag));
668
2/2
✓ Branch 123 → 124 taken 30216 times.
✓ Branch 124 → 125 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.raw));
669
2/2
✓ Branch 129 → 130 taken 30216 times.
✓ Branch 130 → 131 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.baseRaw));
670
2/2
✓ Branch 135 → 136 taken 30216 times.
✓ Branch 136 → 137 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.syncState));
671
2/2
✓ Branch 141 → 142 taken 30216 times.
✓ Branch 142 → 143 taken 30216 times.
30216 insert.addBindValue(sqlValue(card.writable));
672
3/3
✓ Branch 145 → 146 taken 30216 times.
✓ Branch 146 → 147 taken 2 times.
✓ Branch 146 → 163 taken 30214 times.
30216 if (!insert.exec()) {
673
8/8
✓ Branch 147 → 148 taken 2 times.
✓ Branch 151 → 152 taken 2 times.
✓ Branch 152 → 153 taken 2 times.
✓ Branch 153 → 154 taken 2 times.
✓ Branch 154 → 155 taken 2 times.
✓ Branch 155 → 156 taken 2 times.
✓ Branch 161 → 149 taken 2 times.
✓ Branch 161 → 162 taken 2 times.
4 qCWarning(lcContactStore) << "putCard insert failed:" << insert.lastError().text();
674 2 return 0;
675 }
676
2/2
✓ Branch 163 → 164 taken 30214 times.
✓ Branch 164 → 165 taken 30214 times.
30214 id = insert.lastInsertId().toLongLong();
677 30216 }
678
3/3
✓ Branch 172 → 173 taken 30498 times.
✓ Branch 173 → 174 taken 30497 times.
✓ Branch 173 → 175 taken 1 time.
30498 return writeDerived(id, card.raw) ? id : 0;
679 30501 }
680
681 30498 bool ContactStore::writeDerived(qint64 cardId, const QString &raw) {
682
1/1
✓ Branch 2 → 3 taken 30498 times.
30498 ContactCard card;
683 30498 bool writable = true;
684
1/1
✓ Branch 3 → 4 taken 30498 times.
30498 const bool readable = VCardCodec::parse(raw, &card, nullptr, &writable);
685
1/1
✓ Branch 4 → 5 taken 30498 times.
30498 const QString name = ContactIndex::displayName(card);
686
1/1
✓ Branch 5 → 6 taken 30498 times.
30498 const QString folded = ContactIndex::foldForSort(name);
687
1/1
✓ Branch 6 → 7 taken 30498 times.
30498 QSqlQuery q(m_db);
688
1/1
✓ Branch 10 → 11 taken 30498 times.
30498 q.prepare(QStringLiteral(
689 "UPDATE cards SET version = ?, kind = ?, display_name = ?, sort_display = ?, sort_family = ?, section = ?, "
690 "organization = ?, primary_address = ?, address_count = ?, has_photo = ?, search_text = ?, birthday = ?, "
691 "photo_uri = ?, phone = ?, job_title = ?, locality = ?, anniversary = ?, writable = writable AND ? WHERE id = ?"));
692
2/2
✓ Branch 15 → 16 taken 30498 times.
✓ Branch 16 → 17 taken 30498 times.
30498 q.addBindValue(sqlValue(card.version));
693
2/2
✓ Branch 21 → 22 taken 30498 times.
✓ Branch 22 → 23 taken 30498 times.
30498 q.addBindValue(sqlValue(card.kind));
694
2/2
✓ Branch 27 → 28 taken 30498 times.
✓ Branch 28 → 29 taken 30498 times.
30498 q.addBindValue(sqlValue(name));
695
2/2
✓ Branch 33 → 34 taken 30498 times.
✓ Branch 34 → 35 taken 30498 times.
30498 q.addBindValue(sqlValue(folded));
696
4/4
✓ Branch 38 → 39 taken 30498 times.
✓ Branch 39 → 40 taken 30498 times.
✓ Branch 41 → 42 taken 30498 times.
✓ Branch 42 → 43 taken 30498 times.
30498 q.addBindValue(sqlValue(ContactIndex::foldForSort(ContactIndex::familyFirstName(card))));
697
3/3
✓ Branch 48 → 49 taken 30498 times.
✓ Branch 50 → 51 taken 30498 times.
✓ Branch 51 → 52 taken 30498 times.
30498 q.addBindValue(sqlValue(ContactIndex::sectionOf(folded)));
698
2/2
✓ Branch 57 → 58 taken 30498 times.
✓ Branch 58 → 59 taken 30498 times.
30498 q.addBindValue(sqlValue(card.organization));
699
3/3
✓ Branch 62 → 63 taken 30498 times.
✓ Branch 64 → 65 taken 30498 times.
✓ Branch 65 → 66 taken 30498 times.
30498 q.addBindValue(sqlValue(ContactIndex::primaryAddress(card)));
700
2/2
✓ Branch 72 → 73 taken 30498 times.
✓ Branch 73 → 74 taken 30498 times.
30498 q.addBindValue(sqlValue(int(card.emails.size())));
701
2/2
✓ Branch 78 → 79 taken 30498 times.
✓ Branch 79 → 80 taken 30498 times.
30498 q.addBindValue(sqlValue(card.photo.kind != ContactPhoto::Kind::None));
702
3/3
✓ Branch 83 → 84 taken 30498 times.
✓ Branch 85 → 86 taken 30498 times.
✓ Branch 86 → 87 taken 30498 times.
30498 q.addBindValue(sqlValue(ContactIndex::searchText(card)));
703
5/5
✓ Branch 92 → 93 taken 340 times.
✓ Branch 92 → 94 taken 30158 times.
✓ Branch 93 → 95 taken 340 times.
✓ Branch 96 → 97 taken 30498 times.
✓ Branch 97 → 98 taken 30498 times.
30498 q.addBindValue(sqlValue(card.birthday.isValid() ? card.birthday.toIso() : QString()));
704
4/4
✓ Branch 102 → 103 taken 5 times.
✓ Branch 102 → 104 taken 30493 times.
✓ Branch 106 → 107 taken 30498 times.
✓ Branch 107 → 108 taken 30498 times.
30498 q.addBindValue(sqlValue(card.photo.kind == ContactPhoto::Kind::Uri ? card.photo.uri : QString()));
705
3/3
✓ Branch 112 → 113 taken 30498 times.
✓ Branch 114 → 115 taken 30498 times.
✓ Branch 115 → 116 taken 30498 times.
30498 q.addBindValue(sqlValue(ContactIndex::primaryPhone(card)));
706
3/3
✓ Branch 120 → 121 taken 30498 times.
✓ Branch 122 → 123 taken 30498 times.
✓ Branch 123 → 124 taken 30498 times.
30498 q.addBindValue(sqlValue(card.title.trimmed()));
707
3/3
✓ Branch 128 → 129 taken 30498 times.
✓ Branch 130 → 131 taken 30498 times.
✓ Branch 131 → 132 taken 30498 times.
30498 q.addBindValue(sqlValue(ContactIndex::primaryLocality(card)));
708
5/5
✓ Branch 137 → 138 taken 51 times.
✓ Branch 137 → 139 taken 30447 times.
✓ Branch 138 → 140 taken 51 times.
✓ Branch 141 → 142 taken 30498 times.
✓ Branch 142 → 143 taken 30498 times.
30498 q.addBindValue(sqlValue(card.anniversary.isValid() ? card.anniversary.toIso() : QString()));
709
5/6
✓ Branch 147 → 148 taken 30498 times.
✗ Branch 147 → 150 not taken.
✓ Branch 148 → 149 taken 30497 times.
✓ Branch 148 → 150 taken 1 time.
✓ Branch 152 → 153 taken 30498 times.
✓ Branch 153 → 154 taken 30498 times.
30498 q.addBindValue(sqlValue(readable && writable));
710
2/2
✓ Branch 158 → 159 taken 30498 times.
✓ Branch 159 → 160 taken 30498 times.
30498 q.addBindValue(sqlValue(cardId));
711
1/1
✓ Branch 162 → 163 taken 30498 times.
30498 bool ok = q.exec();
712
2/2
✓ Branch 189 → 165 taken 91494 times.
✓ Branch 189 → 190 taken 30498 times.
121992 for (const char *table : {"card_addresses", "card_groups", "card_members"}) {
713
1/1
✓ Branch 165 → 166 taken 91494 times.
91494 QSqlQuery clear(m_db);
714
2/2
✓ Branch 171 → 172 taken 91494 times.
✓ Branch 172 → 173 taken 91494 times.
182988 clear.prepare(QStringLiteral("DELETE FROM %1 WHERE card_id = ?").arg(QLatin1String(table)));
715
2/2
✓ Branch 178 → 179 taken 91494 times.
✓ Branch 179 → 180 taken 91494 times.
91494 clear.addBindValue(sqlValue(cardId));
716
3/5
✓ Branch 182 → 183 taken 91494 times.
✓ Branch 183 → 184 taken 91494 times.
✗ Branch 183 → 186 not taken.
✓ Branch 184 → 185 taken 91494 times.
✗ Branch 184 → 186 not taken.
91494 ok = clear.exec() && ok;
717 91494 }
718
2/2
✓ Branch 257 → 191 taken 29937 times.
✓ Branch 257 → 258 taken 30498 times.
60435 for (int i = 0; i < card.emails.size(); ++i) {
719 29937 const ContactValue &email = card.emails.at(i);
720
2/3
✓ Branch 192 → 193 taken 29937 times.
✗ Branch 195 → 196 not taken.
✓ Branch 195 → 197 taken 29937 times.
29937 if (email.value.trimmed().isEmpty())
721 ✗ continue;
722
1/1
✓ Branch 197 → 198 taken 29937 times.
29937 QSqlQuery insert(m_db);
723
1/1
✓ Branch 201 → 202 taken 29937 times.
29937 insert.prepare(QStringLiteral("INSERT INTO card_addresses (card_id, position, address, address_key, type, preferred) "
724 "VALUES (?, ?, ?, ?, ?, ?)"));
725
2/2
✓ Branch 206 → 207 taken 29937 times.
✓ Branch 207 → 208 taken 29937 times.
29937 insert.addBindValue(sqlValue(cardId));
726
2/2
✓ Branch 212 → 213 taken 29937 times.
✓ Branch 213 → 214 taken 29937 times.
29937 insert.addBindValue(sqlValue(i));
727
3/3
✓ Branch 217 → 218 taken 29937 times.
✓ Branch 219 → 220 taken 29937 times.
✓ Branch 220 → 221 taken 29937 times.
29937 insert.addBindValue(sqlValue(email.value.trimmed()));
728
3/3
✓ Branch 225 → 226 taken 29937 times.
✓ Branch 227 → 228 taken 29937 times.
✓ Branch 228 → 229 taken 29937 times.
29937 insert.addBindValue(sqlValue(ContactIndex::addressKey(email.value)));
729
3/3
✓ Branch 235 → 236 taken 29937 times.
✓ Branch 237 → 238 taken 29937 times.
✓ Branch 238 → 239 taken 29937 times.
29937 insert.addBindValue(sqlValue(email.types.join(QLatin1Char(','))));
730
2/2
✓ Branch 244 → 245 taken 29937 times.
✓ Branch 245 → 246 taken 29937 times.
29937 insert.addBindValue(sqlValue(email.preferred));
731
4/5
✓ Branch 248 → 249 taken 29937 times.
✓ Branch 249 → 250 taken 29936 times.
✓ Branch 249 → 252 taken 1 time.
✓ Branch 250 → 251 taken 29936 times.
✗ Branch 250 → 252 not taken.
29937 ok = insert.exec() && ok;
732 29937 }
733
4/4
✓ Branch 258 → 259 taken 30498 times.
✓ Branch 259 → 260 taken 30498 times.
✓ Branch 289 → 261 taken 423 times.
✓ Branch 289 → 290 taken 30498 times.
30921 for (const QString &group : card.categories) {
734
1/1
✓ Branch 262 → 263 taken 423 times.
423 QSqlQuery insert(m_db);
735
1/1
✓ Branch 266 → 267 taken 423 times.
423 insert.prepare(QStringLiteral("INSERT INTO card_groups (card_id, name) VALUES (?, ?)"));
736
2/2
✓ Branch 271 → 272 taken 423 times.
✓ Branch 272 → 273 taken 423 times.
423 insert.addBindValue(sqlValue(cardId));
737
2/2
✓ Branch 277 → 278 taken 423 times.
✓ Branch 278 → 279 taken 423 times.
423 insert.addBindValue(sqlValue(group));
738
3/5
✓ Branch 281 → 282 taken 423 times.
✓ Branch 282 → 283 taken 423 times.
✗ Branch 282 → 285 not taken.
✓ Branch 283 → 284 taken 423 times.
✗ Branch 283 → 285 not taken.
423 ok = insert.exec() && ok;
739 423 }
740
4/4
✓ Branch 290 → 291 taken 30498 times.
✓ Branch 291 → 292 taken 30498 times.
✓ Branch 321 → 293 taken 146 times.
✓ Branch 321 → 322 taken 30498 times.
30644 for (const QString &member : card.members) {
741
1/1
✓ Branch 294 → 295 taken 146 times.
146 QSqlQuery insert(m_db);
742
1/1
✓ Branch 298 → 299 taken 146 times.
146 insert.prepare(QStringLiteral("INSERT INTO card_members (card_id, member) VALUES (?, ?)"));
743
2/2
✓ Branch 303 → 304 taken 146 times.
✓ Branch 304 → 305 taken 146 times.
146 insert.addBindValue(sqlValue(cardId));
744
2/2
✓ Branch 309 → 310 taken 146 times.
✓ Branch 310 → 311 taken 146 times.
146 insert.addBindValue(sqlValue(member));
745
3/5
✓ Branch 313 → 314 taken 146 times.
✓ Branch 314 → 315 taken 146 times.
✗ Branch 314 → 317 not taken.
✓ Branch 315 → 316 taken 146 times.
✗ Branch 315 → 317 not taken.
146 ok = insert.exec() && ok;
746 146 }
747
4/5
✓ Branch 322 → 323 taken 30498 times.
✓ Branch 323 → 324 taken 30498 times.
✗ Branch 323 → 326 not taken.
✓ Branch 324 → 325 taken 30497 times.
✓ Branch 324 → 326 taken 1 time.
60996 return ContactPeopleStore::writeRows(m_db, cardId, card) && ok;
748 30498 }
749
750 59 bool ContactStore::removeCard(qint64 bookId, const QString &uid) {
751
1/1
✓ Branch 2 → 3 taken 59 times.
59 QSqlQuery q(m_db);
752
1/1
✓ Branch 6 → 7 taken 59 times.
59 q.prepare(QStringLiteral("DELETE FROM cards WHERE book_id = ? AND uid = ?"));
753
2/2
✓ Branch 11 → 12 taken 59 times.
✓ Branch 12 → 13 taken 59 times.
59 q.addBindValue(sqlValue(bookId));
754
2/2
✓ Branch 17 → 18 taken 59 times.
✓ Branch 18 → 19 taken 59 times.
59 q.addBindValue(sqlValue(uid));
755
5/6
✓ Branch 21 → 22 taken 59 times.
✓ Branch 22 → 23 taken 56 times.
✓ Branch 22 → 26 taken 3 times.
✓ Branch 23 → 24 taken 56 times.
✓ Branch 24 → 25 taken 56 times.
✗ Branch 24 → 26 not taken.
118 return q.exec() && q.numRowsAffected() > 0;
756 59 }
757
758 46 bool ContactStore::setSyncState(qint64 bookId, const QString &uid, const QString &state) {
759
1/1
✓ Branch 2 → 3 taken 46 times.
46 QSqlQuery q(m_db);
760
1/1
✓ Branch 6 → 7 taken 46 times.
46 q.prepare(QStringLiteral("UPDATE cards SET sync_state = ? WHERE book_id = ? AND uid = ?"));
761
2/2
✓ Branch 11 → 12 taken 46 times.
✓ Branch 12 → 13 taken 46 times.
46 q.addBindValue(sqlValue(state));
762
2/2
✓ Branch 17 → 18 taken 46 times.
✓ Branch 18 → 19 taken 46 times.
46 q.addBindValue(sqlValue(bookId));
763
2/2
✓ Branch 23 → 24 taken 46 times.
✓ Branch 24 → 25 taken 46 times.
46 q.addBindValue(sqlValue(uid));
764
1/1
✓ Branch 27 → 28 taken 46 times.
92 return q.exec();
765 46 }
766
767 14159 qint64 ContactStore::addLocalCard(const ContactCard &input) {
768 14159 ContactCard card = input;
769
2/2
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 10 taken 14154 times.
14159 if (card.uid.isEmpty())
770
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 6 → 7 taken 5 times.
5 card.uid = QUuid::createUuid().toString(QUuid::WithoutBraces);
771 14159 StoredCard stored;
772
1/1
✓ Branch 10 → 11 taken 14159 times.
14159 stored.bookId = localBookId();
773 14159 stored.uid = card.uid;
774
1/1
✓ Branch 16 → 17 taken 14159 times.
28318 stored.raw = VCardCodec::create(card);
775
2/3
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 14159 times.
✓ Branch 25 → 26 taken 14159 times.
28318 return stored.raw.isEmpty() ? 0 : putCard(stored);
776 14159 }
777
778 // ─── outbox ────────────────────────────────────────────────
779
780 304 qint64 ContactStore::enqueueOutbox(qint64 bookId, const QString &uid, const QString &operation, const QString &href,
781 const QString &baseEtag, qint64 dependsOn) {
782
3/3
✓ Branch 2 → 3 taken 304 times.
✓ Branch 4 → 5 taken 118 times.
✓ Branch 4 → 44 taken 186 times.
304 if (auto existing = outboxEntry(bookId, uid)) {
783 // Coalesce: the newest operation wins; the base stays the one the first
784 // queued change was made on (what the server still has).
785 118 existing->operation = operation;
786 118 existing->revision += 1;
787
2/2
✓ Branch 10 → 11 taken 94 times.
✓ Branch 10 → 13 taken 24 times.
118 if (existing->href.isEmpty())
788 94 existing->href = href;
789 118 existing->attempts = 0;
790 118 existing->nextAttempt = 0;
791 // Sprint 107 F107-INV-1 (as the calendar store, S106-130): a new change
792 // re-opens an entry that ended 'conflict' or 'failed' — it is pushed
793 // again and the conflict is re-evaluated with the newest edit. It used
794 // to stay parked while the card showed "pending" and its banner was gone.
795
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 34 taken 117 times.
118 if (existing->state != QLatin1String("pending")) {
796 1 existing->state = QStringLiteral("pending");
797 1 existing->lastError.clear();
798 1 existing->serverRaw.clear();
799 1 existing->serverEtag.clear();
800
1/1
✓ Branch 33 → 34 taken 1 time.
1 existing->conflictFields.clear();
801 }
802
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 118 times.
118 if (dependsOn > 0)
803 ✗ existing->dependsOn = dependsOn;
804
2/3
✓ Branch 38 → 39 taken 118 times.
✓ Branch 39 → 40 taken 118 times.
✗ Branch 39 → 42 not taken.
118 return updateOutbox(*existing) ? existing->id : 0;
805 304 }
806
1/1
✓ Branch 48 → 50 taken 186 times.
186 QSqlQuery q(m_db);
807
1/1
✓ Branch 53 → 54 taken 186 times.
186 q.prepare(QStringLiteral("INSERT INTO contact_outbox (book_id, uid, operation, href, base_etag, depends_on) "
808 "VALUES (?, ?, ?, ?, ?, ?)"));
809
2/2
✓ Branch 58 → 59 taken 186 times.
✓ Branch 59 → 60 taken 186 times.
186 q.addBindValue(sqlValue(bookId));
810
2/2
✓ Branch 64 → 65 taken 186 times.
✓ Branch 65 → 66 taken 186 times.
186 q.addBindValue(sqlValue(uid));
811
2/2
✓ Branch 70 → 71 taken 186 times.
✓ Branch 71 → 72 taken 186 times.
186 q.addBindValue(sqlValue(operation));
812
2/2
✓ Branch 76 → 77 taken 186 times.
✓ Branch 77 → 78 taken 186 times.
186 q.addBindValue(sqlValue(href));
813
2/2
✓ Branch 82 → 83 taken 186 times.
✓ Branch 83 → 84 taken 186 times.
186 q.addBindValue(sqlValue(baseEtag));
814
2/2
✓ Branch 88 → 89 taken 186 times.
✓ Branch 89 → 90 taken 186 times.
186 q.addBindValue(sqlValue(dependsOn));
815
3/3
✓ Branch 92 → 93 taken 186 times.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 110 taken 185 times.
186 if (!q.exec()) {
816
8/8
✓ Branch 94 → 95 taken 1 time.
✓ Branch 98 → 99 taken 1 time.
✓ Branch 99 → 100 taken 1 time.
✓ Branch 100 → 101 taken 1 time.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
✓ Branch 108 → 96 taken 1 time.
✓ Branch 108 → 109 taken 1 time.
2 qCWarning(lcContactStore) << "enqueueOutbox failed:" << q.lastError().text();
817 1 return 0;
818 }
819
2/2
✓ Branch 110 → 111 taken 185 times.
✓ Branch 111 → 112 taken 185 times.
185 return q.lastInsertId().toLongLong();
820 186 }
821
822 1941 QList<ContactOutboxEntry> ContactStore::outboxEntries() const {
823 1941 QList<ContactOutboxEntry> result;
824
1/1
✓ Branch 2 → 3 taken 1941 times.
1941 QSqlQuery q(m_db);
825
2/2
✓ Branch 8 → 9 taken 1941 times.
✓ Branch 9 → 10 taken 1941 times.
3882 q.exec(QStringLiteral("SELECT %1 FROM contact_outbox ORDER BY id").arg(QLatin1String(kOutboxColumns)));
826
3/3
✓ Branch 18 → 19 taken 3133 times.
✓ Branch 19 → 14 taken 1192 times.
✓ Branch 19 → 20 taken 1941 times.
3133 while (q.next())
827
2/2
✓ Branch 14 → 15 taken 1192 times.
✓ Branch 15 → 16 taken 1192 times.
1192 result.append(outboxFrom(q));
828 1941 return result;
829 1941 }
830
831 6942 std::optional<ContactOutboxEntry> ContactStore::outboxEntry(qint64 bookId, const QString &uid) const {
832
1/1
✓ Branch 2 → 3 taken 6942 times.
6942 QSqlQuery q(m_db);
833
1/1
✓ Branch 9 → 10 taken 6942 times.
20826 q.prepare(QStringLiteral("SELECT %1 FROM contact_outbox WHERE book_id = ? AND uid = ?")
834
1/1
✓ Branch 8 → 9 taken 6942 times.
20826 .arg(QLatin1String(kOutboxColumns)));
835
2/2
✓ Branch 15 → 16 taken 6942 times.
✓ Branch 16 → 17 taken 6942 times.
6942 q.addBindValue(sqlValue(bookId));
836
2/2
✓ Branch 21 → 22 taken 6942 times.
✓ Branch 22 → 23 taken 6942 times.
6942 q.addBindValue(sqlValue(uid));
837
8/8
✓ Branch 25 → 26 taken 6942 times.
✓ Branch 26 → 27 taken 6941 times.
✓ Branch 26 → 30 taken 1 time.
✓ Branch 27 → 28 taken 6941 times.
✓ Branch 28 → 29 taken 748 times.
✓ Branch 28 → 30 taken 6193 times.
✓ Branch 31 → 32 taken 748 times.
✓ Branch 31 → 36 taken 6194 times.
6942 if (q.exec() && q.next())
838
1/1
✓ Branch 32 → 33 taken 748 times.
748 return outboxFrom(q);
839 6194 return std::nullopt;
840 6942 }
841
842 ✗ std::optional<ContactOutboxEntry> ContactStore::outboxEntryById(qint64 id) const {
843 ✗ QSqlQuery q(m_db);
844 ✗ q.prepare(QStringLiteral("SELECT %1 FROM contact_outbox WHERE id = ?").arg(QLatin1String(kOutboxColumns)));
845 ✗ q.addBindValue(sqlValue(id));
846 ✗ if (q.exec() && q.next())
847 ✗ return outboxFrom(q);
848 ✗ return std::nullopt;
849 ✗ }
850
851 225 bool ContactStore::updateOutbox(const ContactOutboxEntry &entry) {
852
1/1
✓ Branch 2 → 3 taken 225 times.
225 QSqlQuery q(m_db);
853
1/1
✓ Branch 6 → 7 taken 225 times.
225 q.prepare(QStringLiteral(
854 "UPDATE contact_outbox SET operation = ?, href = ?, base_etag = ?, revision = ?, attempts = ?, "
855 "next_attempt = ?, last_error = ?, state = ?, server_raw = ?, server_etag = ?, conflict_fields = ?, "
856 "depends_on = ?, base_raw = ? WHERE id = ?"));
857
2/2
✓ Branch 11 → 12 taken 225 times.
✓ Branch 12 → 13 taken 225 times.
225 q.addBindValue(sqlValue(entry.operation));
858
2/2
✓ Branch 17 → 18 taken 225 times.
✓ Branch 18 → 19 taken 225 times.
225 q.addBindValue(sqlValue(entry.href));
859
2/2
✓ Branch 23 → 24 taken 225 times.
✓ Branch 24 → 25 taken 225 times.
225 q.addBindValue(sqlValue(entry.baseEtag));
860
2/2
✓ Branch 29 → 30 taken 225 times.
✓ Branch 30 → 31 taken 225 times.
225 q.addBindValue(sqlValue(entry.revision));
861
2/2
✓ Branch 35 → 36 taken 225 times.
✓ Branch 36 → 37 taken 225 times.
225 q.addBindValue(sqlValue(entry.attempts));
862
2/2
✓ Branch 41 → 42 taken 225 times.
✓ Branch 42 → 43 taken 225 times.
225 q.addBindValue(sqlValue(entry.nextAttempt));
863
2/2
✓ Branch 47 → 48 taken 225 times.
✓ Branch 48 → 49 taken 225 times.
225 q.addBindValue(sqlValue(entry.lastError));
864
2/2
✓ Branch 53 → 54 taken 225 times.
✓ Branch 54 → 55 taken 225 times.
225 q.addBindValue(sqlValue(entry.state));
865
2/2
✓ Branch 59 → 60 taken 225 times.
✓ Branch 60 → 61 taken 225 times.
225 q.addBindValue(sqlValue(entry.serverRaw));
866
2/2
✓ Branch 65 → 66 taken 225 times.
✓ Branch 66 → 67 taken 225 times.
225 q.addBindValue(sqlValue(entry.serverEtag));
867
3/3
✓ Branch 72 → 73 taken 225 times.
✓ Branch 74 → 75 taken 225 times.
✓ Branch 75 → 76 taken 225 times.
225 q.addBindValue(sqlValue(entry.conflictFields.join(QLatin1Char(','))));
868
2/2
✓ Branch 81 → 82 taken 225 times.
✓ Branch 82 → 83 taken 225 times.
225 q.addBindValue(sqlValue(entry.dependsOn));
869
2/2
✓ Branch 87 → 88 taken 225 times.
✓ Branch 88 → 89 taken 225 times.
225 q.addBindValue(sqlValue(entry.baseRaw));
870
2/2
✓ Branch 93 → 94 taken 225 times.
✓ Branch 94 → 95 taken 225 times.
225 q.addBindValue(sqlValue(entry.id));
871
1/1
✓ Branch 97 → 98 taken 225 times.
450 return q.exec();
872 225 }
873
874 83 bool ContactStore::removeOutbox(qint64 bookId, const QString &uid, int revision) {
875
1/1
✓ Branch 2 → 3 taken 83 times.
83 QSqlQuery q(m_db);
876
2/2
✓ Branch 3 → 4 taken 47 times.
✓ Branch 3 → 29 taken 36 times.
83 if (revision >= 0) {
877
1/1
✓ Branch 7 → 8 taken 47 times.
47 q.prepare(QStringLiteral("DELETE FROM contact_outbox WHERE book_id = ? AND uid = ? AND revision = ?"));
878
2/2
✓ Branch 12 → 13 taken 47 times.
✓ Branch 13 → 14 taken 47 times.
47 q.addBindValue(sqlValue(bookId));
879
2/2
✓ Branch 18 → 19 taken 47 times.
✓ Branch 19 → 20 taken 47 times.
47 q.addBindValue(sqlValue(uid));
880
2/2
✓ Branch 24 → 25 taken 47 times.
✓ Branch 25 → 26 taken 47 times.
47 q.addBindValue(sqlValue(revision));
881 } else {
882
1/1
✓ Branch 32 → 33 taken 36 times.
36 q.prepare(QStringLiteral("DELETE FROM contact_outbox WHERE book_id = ? AND uid = ?"));
883
2/2
✓ Branch 37 → 38 taken 36 times.
✓ Branch 38 → 39 taken 36 times.
36 q.addBindValue(sqlValue(bookId));
884
2/2
✓ Branch 43 → 44 taken 36 times.
✓ Branch 44 → 45 taken 36 times.
36 q.addBindValue(sqlValue(uid));
885 }
886
6/6
✓ Branch 48 → 49 taken 83 times.
✓ Branch 49 → 50 taken 82 times.
✓ Branch 49 → 53 taken 1 time.
✓ Branch 50 → 51 taken 82 times.
✓ Branch 51 → 52 taken 66 times.
✓ Branch 51 → 53 taken 16 times.
166 return q.exec() && q.numRowsAffected() > 0;
887 83 }
888
889 // ─── meta and legacy ───────────────────────────────────────
890
891 345 QString ContactStore::meta(const QString &key) const {
892
1/1
✓ Branch 2 → 3 taken 345 times.
345 QSqlQuery q(m_db);
893
1/1
✓ Branch 6 → 7 taken 345 times.
345 q.prepare(QStringLiteral("SELECT value FROM contact_meta WHERE key = ?"));
894
2/2
✓ Branch 11 → 12 taken 345 times.
✓ Branch 12 → 13 taken 345 times.
345 q.addBindValue(sqlValue(key));
895
8/8
✓ Branch 15 → 16 taken 345 times.
✓ Branch 16 → 17 taken 344 times.
✓ Branch 16 → 20 taken 1 time.
✓ Branch 17 → 18 taken 344 times.
✓ Branch 18 → 19 taken 16 times.
✓ Branch 18 → 20 taken 328 times.
✓ Branch 21 → 22 taken 16 times.
✓ Branch 21 → 27 taken 329 times.
345 if (q.exec() && q.next())
896
2/2
✓ Branch 22 → 23 taken 16 times.
✓ Branch 23 → 24 taken 16 times.
16 return q.value(0).toString();
897 329 return {};
898 345 }
899
900 25 bool ContactStore::setMeta(const QString &key, const QString &value) {
901
1/1
✓ Branch 2 → 3 taken 25 times.
25 QSqlQuery q(m_db);
902
1/1
✓ Branch 6 → 7 taken 25 times.
25 q.prepare(QStringLiteral("INSERT OR REPLACE INTO contact_meta (key, value) VALUES (?, ?)"));
903
2/2
✓ Branch 11 → 12 taken 25 times.
✓ Branch 12 → 13 taken 25 times.
25 q.addBindValue(sqlValue(key));
904
2/2
✓ Branch 17 → 18 taken 25 times.
✓ Branch 18 → 19 taken 25 times.
25 q.addBindValue(sqlValue(value));
905
1/1
✓ Branch 21 → 22 taken 25 times.
50 return q.exec();
906 25 }
907
908 75 QList<ContactStore::PendingPhoto> ContactStore::pendingPhotos(qint64 bookId, int limit) const {
909
1/1
✓ Branch 2 → 3 taken 75 times.
75 QSqlQuery q(m_db);
910
1/1
✓ Branch 6 → 7 taken 75 times.
75 q.prepare(QStringLiteral("SELECT id, uid, href, photo_uri FROM cards WHERE book_id = ? AND photo_uri <> '' "
911 "AND photo_fetched_uri <> photo_uri ORDER BY id LIMIT ?"));
912
2/2
✓ Branch 11 → 12 taken 75 times.
✓ Branch 12 → 13 taken 75 times.
75 q.addBindValue(sqlValue(bookId));
913
2/2
✓ Branch 17 → 18 taken 75 times.
✓ Branch 18 → 19 taken 75 times.
75 q.addBindValue(sqlValue(limit));
914 75 QList<PendingPhoto> pending;
915
2/3
✓ Branch 21 → 22 taken 75 times.
✓ Branch 22 → 23 taken 75 times.
✗ Branch 22 → 41 not taken.
75 if (q.exec())
916
3/3
✓ Branch 39 → 40 taken 80 times.
✓ Branch 40 → 24 taken 5 times.
✓ Branch 40 → 41 taken 75 times.
80 while (q.next())
917
2/2
✓ Branch 24 → 25 taken 5 times.
✓ Branch 25 → 26 taken 5 times.
10 pending.append({q.value(0).toLongLong(), q.value(1).toString(), q.value(2).toString(), q.value(3).toString()});
918 75 return pending;
919 80 }
920
921 6 bool ContactStore::setFetchedPhoto(qint64 cardId, const QString &uri, const QByteArray &bytes) {
922
1/1
✓ Branch 2 → 3 taken 6 times.
6 QSqlQuery q(m_db);
923
1/1
✓ Branch 6 → 7 taken 6 times.
6 q.prepare(QStringLiteral("UPDATE cards SET photo_fetched_uri = ?, photo_thumb = ?, updated_at = datetime('now') "
924 "WHERE id = ? AND photo_uri = ?"));
925
2/2
✓ Branch 11 → 12 taken 6 times.
✓ Branch 12 → 13 taken 6 times.
6 q.addBindValue(sqlValue(uri));
926
5/5
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 20 taken 4 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 19 → 21 taken 2 times.
✓ Branch 21 → 22 taken 6 times.
6 q.addBindValue(bytes.isEmpty() ? QVariant(QMetaType(QMetaType::QByteArray)) : QVariant(bytes));
927
2/2
✓ Branch 25 → 26 taken 6 times.
✓ Branch 26 → 27 taken 6 times.
6 q.addBindValue(sqlValue(cardId));
928
2/2
✓ Branch 31 → 32 taken 6 times.
✓ Branch 32 → 33 taken 6 times.
6 q.addBindValue(sqlValue(uri));
929
5/6
✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 37 taken 6 times.
✗ Branch 36 → 40 not taken.
✓ Branch 37 → 38 taken 6 times.
✓ Branch 38 → 39 taken 5 times.
✓ Branch 38 → 40 taken 1 time.
12 return q.exec() && q.numRowsAffected() == 1;
930 6 }
931
932 7 QByteArray ContactStore::fetchedPhoto(qint64 bookId, const QString &uid) const {
933
1/1
✓ Branch 2 → 3 taken 7 times.
7 QSqlQuery q(m_db);
934
1/1
✓ Branch 6 → 7 taken 7 times.
7 q.prepare(QStringLiteral("SELECT photo_thumb FROM cards WHERE book_id = ? AND uid = ? AND photo_uri <> '' "
935 "AND photo_fetched_uri = photo_uri"));
936
2/2
✓ Branch 11 → 12 taken 7 times.
✓ Branch 12 → 13 taken 7 times.
7 q.addBindValue(sqlValue(bookId));
937
2/2
✓ Branch 17 → 18 taken 7 times.
✓ Branch 18 → 19 taken 7 times.
7 q.addBindValue(sqlValue(uid));
938
9/12
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
✗ Branch 22 → 28 not taken.
✓ Branch 23 → 24 taken 7 times.
✓ Branch 24 → 25 taken 6 times.
✓ Branch 24 → 28 taken 1 time.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 26 → 27 taken 6 times.
✓ Branch 30 → 31 taken 6 times.
✓ Branch 30 → 32 taken 1 time.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
21 return q.exec() && q.next() ? q.value(0).toByteArray() : QByteArray();
939 7 }
940
941 // ─── Sprint 107 D-21: conflict history ─────────────────────
942
943 16 bool ContactStore::addConflictHistory(const ConflictHistory::Entry &entry, qint64 now) {
944 16 return ConflictHistory::add(m_db, entry, now);
945 }
946
947 890 QList<ConflictHistory::Entry> ContactStore::conflictHistory(int limit) const {
948 890 return ConflictHistory::list(m_db, limit);
949 }
950
951 3 std::optional<ConflictHistory::Entry> ContactStore::conflictHistoryEntry(qint64 id) const {
952 3 return ConflictHistory::find(m_db, id);
953 }
954
955 2 bool ContactStore::removeConflictHistory(qint64 id) { return ConflictHistory::remove(m_db, id); }
956
957 4 QList<qint64> ContactStore::booksWithCard(const QString &uid) const {
958 4 QList<qint64> books;
959
1/1
✓ Branch 2 → 3 taken 4 times.
4 QSqlQuery q(m_db);
960
1/1
✓ Branch 6 → 7 taken 4 times.
4 q.prepare(QStringLiteral("SELECT book_id FROM cards WHERE uid = ? ORDER BY book_id"));
961
2/2
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
4 q.addBindValue(sqlValue(uid));
962
2/3
✓ Branch 15 → 16 taken 4 times.
✓ Branch 16 → 17 taken 4 times.
✗ Branch 16 → 25 not taken.
4 if (q.exec())
963
3/3
✓ Branch 23 → 24 taken 7 times.
✓ Branch 24 → 18 taken 3 times.
✓ Branch 24 → 25 taken 4 times.
7 while (q.next())
964
3/3
✓ Branch 18 → 19 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
3 books.append(q.value(0).toLongLong());
965 4 return books;
966 4 }
967