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: 94.3% 382 / 0 / 405
Functions: 93.1% 27 / 0 / 29
Branches: 85.3% 561 / 0 / 658

data/ContactQueries.cpp
Line Branch Exec Source
1 #include "ContactQueries.h"
2
3 #include "ContactPeopleFilter.h"
4
5 #include "ContactIndex.h"
6 #include "ContactSqlValue.h"
7
8 #include <QDateTime>
9 #include <QSet>
10 #include <QSqlQuery>
11 #include <algorithm>
12
13 namespace ContactQueries {
14
15 namespace {
16
17 const char *kRowColumns =
18 "c.id, c.book_id, c.uid, c.display_name, c.sort_display, c.sort_family, c.organization, c.primary_address, "
19 "c.address_count, b.display_name, b.account_id, b.read_only, (b.account_id = '' AND b.path = 'local'), "
20 "c.kind, c.sync_state, c.writable, c.has_photo, "
21 "CASE WHEN c.has_photo THEN c.updated_at || ' ' || c.photo_fetched_uri ELSE '' END, "
22 "c.phone, c.job_title, c.locality, c.birthday, "
23 "(SELECT GROUP_CONCAT(g.name, ', ') FROM card_groups g WHERE g.card_id = c.id)";
24
25 // F100-11: a card in no CATEGORIES group and no member of a group card.
26 const char *kUngrouped =
27 "NOT EXISTS (SELECT 1 FROM card_groups g WHERE g.card_id = c.id) AND "
28 "NOT EXISTS (SELECT 1 FROM cards gc JOIN card_members m ON m.card_id = gc.id WHERE gc.book_id = c.book_id"
29 " AND gc.kind = 'group' AND (m.member = 'urn:uuid:' || c.uid OR m.member = c.uid))";
30
31 262 QString likePattern(const QString &word) {
32 262 QString escaped = word;
33
1/1
✓ Branch 8 → 9 taken 262 times.
262 escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
34
1/1
✓ Branch 16 → 17 taken 262 times.
262 escaped.replace(QLatin1Char('%'), QStringLiteral("\\%"));
35
1/1
✓ Branch 24 → 25 taken 262 times.
262 escaped.replace(QLatin1Char('_'), QStringLiteral("\\_"));
36
2/2
✓ Branch 31 → 32 taken 262 times.
✓ Branch 32 → 33 taken 262 times.
786 return QLatin1Char('%') + escaped + QLatin1Char('%');
37 262 }
38
39 2977 QStringList filterWords(const QString &filter) {
40
2/2
✓ Branch 2 → 3 taken 2977 times.
✓ Branch 6 → 7 taken 2977 times.
2977 return ContactIndex::foldForSort(filter).split(QLatin1Char(' '), Qt::SkipEmptyParts);
41 }
42
43 5512 Row rowFrom(const QSqlQuery &q, bool byFamilyName) {
44 5512 Row row;
45
2/2
✓ Branch 2 → 3 taken 5512 times.
✓ Branch 3 → 4 taken 5512 times.
5512 row.cardId = q.value(0).toLongLong();
46
2/2
✓ Branch 5 → 6 taken 5512 times.
✓ Branch 6 → 7 taken 5512 times.
5512 row.bookId = q.value(1).toLongLong();
47
2/2
✓ Branch 8 → 9 taken 5512 times.
✓ Branch 9 → 10 taken 5512 times.
5512 row.uid = q.value(2).toString();
48
1/1
✓ Branch 13 → 14 taken 5512 times.
5512 row.key = cardKey(row.bookId, row.uid);
49
2/2
✓ Branch 16 → 17 taken 5512 times.
✓ Branch 17 → 18 taken 5512 times.
5512 row.displayName = q.value(3).toString();
50
10/14
✓ Branch 21 → 22 taken 16 times.
✓ Branch 21 → 24 taken 5496 times.
✓ Branch 22 → 23 taken 16 times.
✓ Branch 23 → 26 taken 16 times.
✓ Branch 24 → 25 taken 5496 times.
✓ Branch 25 → 26 taken 5496 times.
✓ Branch 28 → 29 taken 5496 times.
✓ Branch 28 → 30 taken 16 times.
✓ Branch 30 → 31 taken 16 times.
✓ Branch 30 → 32 taken 5496 times.
✗ Branch 135 → 136 not taken.
✗ Branch 135 → 137 not taken.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
5512 row.sortKey = byFamilyName ? q.value(5).toString() : q.value(4).toString();
51
1/1
✓ Branch 32 → 33 taken 5512 times.
5512 row.section = ContactIndex::sectionOf(row.sortKey);
52
2/2
✓ Branch 35 → 36 taken 5512 times.
✓ Branch 36 → 37 taken 5512 times.
5512 row.organization = q.value(6).toString();
53
2/2
✓ Branch 40 → 41 taken 5512 times.
✓ Branch 41 → 42 taken 5512 times.
5512 row.primaryAddress = q.value(7).toString();
54
2/2
✓ Branch 45 → 46 taken 5512 times.
✓ Branch 46 → 47 taken 5512 times.
5512 row.addressCount = q.value(8).toInt();
55
2/2
✓ Branch 48 → 49 taken 5512 times.
✓ Branch 49 → 50 taken 5512 times.
5512 row.bookName = q.value(9).toString();
56
2/2
✓ Branch 53 → 54 taken 5512 times.
✓ Branch 54 → 55 taken 5512 times.
5512 row.accountId = q.value(10).toString();
57
2/2
✓ Branch 58 → 59 taken 5512 times.
✓ Branch 59 → 60 taken 5512 times.
5512 row.bookReadOnly = q.value(11).toBool();
58
2/2
✓ Branch 61 → 62 taken 5512 times.
✓ Branch 62 → 63 taken 5512 times.
5512 row.localBook = q.value(12).toBool();
59
2/2
✓ Branch 64 → 65 taken 5512 times.
✓ Branch 65 → 66 taken 5512 times.
5512 row.kind = q.value(13).toString();
60
2/2
✓ Branch 69 → 70 taken 5512 times.
✓ Branch 70 → 71 taken 5512 times.
5512 row.syncState = q.value(14).toString();
61
7/12
✓ Branch 74 → 75 taken 5512 times.
✓ Branch 75 → 76 taken 5512 times.
✓ Branch 76 → 77 taken 5512 times.
✗ Branch 76 → 82 not taken.
✓ Branch 77 → 78 taken 5433 times.
✓ Branch 77 → 82 taken 79 times.
✓ Branch 80 → 81 taken 5433 times.
✗ Branch 80 → 82 not taken.
✓ Branch 83 → 84 taken 5512 times.
✗ Branch 83 → 85 not taken.
✗ Branch 179 → 180 not taken.
✗ Branch 179 → 181 not taken.
5512 row.writable = q.value(15).toBool() && !row.bookReadOnly && row.syncState != QLatin1String("legacy");
62
2/2
✓ Branch 85 → 86 taken 5512 times.
✓ Branch 86 → 87 taken 5512 times.
5512 row.hasPhoto = q.value(16).toBool();
63
2/2
✓ Branch 88 → 89 taken 5512 times.
✓ Branch 89 → 90 taken 5512 times.
5512 row.photoTag = q.value(17).toString();
64
2/2
✓ Branch 93 → 94 taken 5512 times.
✓ Branch 94 → 95 taken 5512 times.
5512 row.phone = q.value(18).toString();
65
2/2
✓ Branch 98 → 99 taken 5512 times.
✓ Branch 99 → 100 taken 5512 times.
5512 row.title = q.value(19).toString();
66
2/2
✓ Branch 103 → 104 taken 5512 times.
✓ Branch 104 → 105 taken 5512 times.
5512 row.locality = q.value(20).toString();
67
2/2
✓ Branch 108 → 109 taken 5512 times.
✓ Branch 109 → 110 taken 5512 times.
5512 row.birthday = q.value(21).toString();
68
2/2
✓ Branch 113 → 114 taken 5512 times.
✓ Branch 114 → 115 taken 5512 times.
5512 row.groups = q.value(22).toString();
69 5512 return row;
70 ✗ }
71
72 // WHERE clause of a card view (without the filter); binds via `binds`.
73 2039 QString viewCondition(const View &view, QVariantMap *binds) {
74
5/5
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 16 taken 5 times.
✓ Branch 2 → 37 taken 1 time.
✓ Branch 2 → 54 taken 1 time.
✓ Branch 2 → 59 taken 2028 times.
2039 switch (view.kind) {
75 4 case ViewKind::Book:
76
1/1
✓ Branch 7 → 8 taken 4 times.
8 binds->insert(QStringLiteral(":book"), view.bookId);
77 4 return QStringLiteral("c.book_id = :book AND c.kind <> 'group'");
78 5 case ViewKind::Group:
79
1/1
✓ Branch 20 → 21 taken 5 times.
10 binds->insert(QStringLiteral(":book"), view.bookId);
80
1/1
✓ Branch 28 → 29 taken 5 times.
10 binds->insert(QStringLiteral(":group"), view.group);
81 5 return QStringLiteral(
82 "c.book_id = :book AND c.kind <> 'group' AND ("
83 " EXISTS (SELECT 1 FROM card_groups g WHERE g.card_id = c.id AND g.name = :group) OR"
84 " EXISTS (SELECT 1 FROM cards gc JOIN card_members m ON m.card_id = gc.id WHERE gc.book_id = c.book_id"
85 " AND gc.kind = 'group' AND gc.display_name = :group"
86 " AND (m.member = 'urn:uuid:' || c.uid OR m.member = c.uid)))");
87 1 case ViewKind::Ungrouped:
88
1/1
✓ Branch 41 → 42 taken 1 time.
2 binds->insert(QStringLiteral(":book"), view.bookId);
89
1/1
✓ Branch 50 → 51 taken 1 time.
2 return QStringLiteral("c.book_id = :book AND c.kind <> 'group' AND %1").arg(QLatin1String(kUngrouped));
90 1 case ViewKind::Duplicates:
91 1 return QStringLiteral(
92 "b.show_in_all = 1 AND c.kind <> 'group' AND EXISTS (SELECT 1 FROM cards o WHERE o.book_id = c.book_id"
93 " AND o.id <> c.id AND o.kind <> 'group' AND ((c.sort_display <> '' AND o.sort_display = c.sort_display) OR"
94 " EXISTS (SELECT 1 FROM card_addresses a JOIN card_addresses oa ON oa.address_key = a.address_key"
95 " WHERE a.card_id = c.id AND oa.card_id = o.id)))");
96 2028 default:
97 2028 return QStringLiteral("b.show_in_all = 1 AND c.kind <> 'group'");
98 }
99 }
100
101 2039 QString filterCondition(const QString &filter, QVariantMap *binds) {
102 // Sprint 103 S103.6.7: related:, via:, met:, near:, interest:, has: first.
103
1/1
✓ Branch 2 → 3 taken 2039 times.
2039 const ContactPeopleFilter::Split people = ContactPeopleFilter::split(filter, binds);
104 2039 QStringList parts = people.conditions;
105
1/1
✓ Branch 4 → 5 taken 2039 times.
2039 const QStringList words = filterWords(people.rest);
106
2/2
✓ Branch 31 → 6 taken 150 times.
✓ Branch 31 → 32 taken 2039 times.
2189 for (int i = 0; i < words.size(); ++i) {
107
1/1
✓ Branch 10 → 11 taken 150 times.
300 const QString name = QStringLiteral(":w%1").arg(i);
108
2/2
✓ Branch 17 → 18 taken 150 times.
✓ Branch 18 → 19 taken 150 times.
300 parts << QStringLiteral("c.search_text LIKE %1 ESCAPE '\\'").arg(name);
109
2/2
✓ Branch 23 → 24 taken 150 times.
✓ Branch 25 → 26 taken 150 times.
150 binds->insert(name, likePattern(words.at(i)));
110 150 }
111
1/1
✓ Branch 35 → 36 taken 2039 times.
4078 return parts.join(QStringLiteral(" AND "));
112 2039 }
113
114 2039 void bindAll(QSqlQuery *q, const QVariantMap &binds) {
115
4/4
✓ Branch 2 → 3 taken 2039 times.
✓ Branch 11 → 12 taken 2252 times.
✓ Branch 13 → 4 taken 213 times.
✓ Branch 13 → 14 taken 2039 times.
2252 for (auto it = binds.cbegin(); it != binds.cend(); ++it)
116
2/2
✓ Branch 6 → 7 taken 213 times.
✓ Branch 8 → 9 taken 213 times.
213 q->bindValue(it.key(), sqlValue(it.value()));
117 2039 }
118
119 // Collected addresses (not in any card) as list rows, sorted.
120 63 QList<Row> collectedRows(QSqlDatabase db, const QString &filter, const QSet<QString> &onlyKeys = {}) {
121 63 QList<Row> rows;
122
1/1
✓ Branch 2 → 3 taken 63 times.
63 QSqlQuery q(db);
123
1/1
✓ Branch 6 → 7 taken 63 times.
63 q.exec(QStringLiteral(
124 "SELECT ca.address_key, ca.address, ca.display_name, COALESCE(u.use_count, 0) FROM collected_addresses ca "
125 "LEFT JOIN address_usage u ON u.address_key = ca.address_key "
126 "WHERE ca.address_key NOT IN (SELECT address_key FROM card_addresses)"));
127
1/1
✓ Branch 9 → 10 taken 63 times.
63 const QStringList words = filterWords(filter);
128
3/3
✓ Branch 79 → 80 taken 497 times.
✓ Branch 80 → 11 taken 434 times.
✓ Branch 80 → 81 taken 63 times.
497 while (q.next()) {
129
10/16
✓ Branch 12 → 13 taken 117 times.
✓ Branch 12 → 18 taken 317 times.
✓ Branch 13 → 14 taken 117 times.
✓ Branch 14 → 15 taken 117 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 117 times.
✓ Branch 19 → 20 taken 117 times.
✓ Branch 19 → 21 taken 317 times.
✓ Branch 21 → 22 taken 117 times.
✓ Branch 21 → 23 taken 317 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 434 times.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
434 if (!onlyKeys.isEmpty() && !onlyKeys.contains(q.value(0).toString()))
130 ✗ continue;
131 434 Row row;
132 434 row.collected = true;
133
3/3
✓ Branch 25 → 26 taken 434 times.
✓ Branch 26 → 27 taken 434 times.
✓ Branch 30 → 31 taken 434 times.
868 row.key = QStringLiteral("collected:") + q.value(0).toString();
134
2/2
✓ Branch 37 → 38 taken 434 times.
✓ Branch 38 → 39 taken 434 times.
434 row.primaryAddress = q.value(1).toString();
135
8/10
✓ Branch 42 → 43 taken 434 times.
✓ Branch 43 → 44 taken 434 times.
✓ Branch 45 → 46 taken 31 times.
✓ Branch 45 → 47 taken 403 times.
✓ Branch 47 → 48 taken 403 times.
✓ Branch 48 → 49 taken 403 times.
✓ Branch 51 → 52 taken 403 times.
✓ Branch 51 → 53 taken 31 times.
✗ Branch 119 → 120 not taken.
✗ Branch 119 → 121 not taken.
434 row.displayName = q.value(2).toString().isEmpty() ? row.primaryAddress : q.value(2).toString();
136
1/1
✓ Branch 55 → 56 taken 434 times.
434 row.sortKey = ContactIndex::foldForSort(row.displayName);
137
1/1
✓ Branch 58 → 59 taken 434 times.
434 row.section = ContactIndex::sectionOf(row.sortKey);
138 434 row.addressCount = 1;
139 434 row.writable = false;
140
2/2
✓ Branch 61 → 62 taken 434 times.
✓ Branch 62 → 63 taken 434 times.
434 row.useCount = q.value(3).toInt();
141
3/3
✓ Branch 66 → 67 taken 434 times.
✓ Branch 67 → 68 taken 434 times.
✓ Branch 68 → 69 taken 434 times.
434 const QString haystack = ContactIndex::foldForSort(row.displayName + QLatin1Char(' ') + row.primaryAddress);
142
2/3
✓ Branch 73 → 74 taken 434 times.
✓ Branch 74 → 75 taken 434 times.
✗ Branch 74 → 76 not taken.
434 if (std::all_of(words.cbegin(), words.cend(), [&](const QString &w) { return haystack.contains(w); }))
143
1/1
✓ Branch 75 → 76 taken 434 times.
434 rows.append(row);
144 434 }
145
3/3
✓ Branch 81 → 82 taken 63 times.
✓ Branch 82 → 83 taken 63 times.
✓ Branch 83 → 84 taken 63 times.
63 std::sort(rows.begin(), rows.end(), [](const Row &a, const Row &b) {
146
2/2
✓ Branch 3 → 4 taken 2172 times.
✓ Branch 3 → 7 taken 93 times.
2265 return a.sortKey == b.sortKey ? a.key < b.key : sortKeyLess(a.sortKey, b.sortKey);
147 });
148 63 return rows;
149 63 }
150
151 // D-9: the 25 addresses used most within 180 days, as card or collected rows.
152 875 QList<Row> frequentRows(QSqlDatabase db, const QString &filter) {
153
1/1
✓ Branch 2 → 3 taken 875 times.
875 QSqlQuery top(db);
154
1/1
✓ Branch 6 → 7 taken 875 times.
875 top.prepare(QStringLiteral("SELECT address_key, use_count FROM address_usage WHERE use_count > 0 AND last_used >= ? "
155 "ORDER BY use_count DESC, last_used DESC LIMIT 25"));
156
5/5
✓ Branch 10 → 11 taken 875 times.
✓ Branch 11 → 12 taken 875 times.
✓ Branch 12 → 13 taken 875 times.
✓ Branch 14 → 15 taken 875 times.
✓ Branch 15 → 16 taken 875 times.
875 top.addBindValue(sqlValue(QDateTime::currentDateTimeUtc().addDays(-180).toString(Qt::ISODate)));
157 875 QList<Row> rows;
158 875 QSet<QString> seenCards;
159 875 QSet<QString> collectedKeys;
160
2/3
✓ Branch 23 → 24 taken 875 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 875 times.
875 if (!top.exec())
161 ✗ return rows;
162 875 QHash<QString, int> usage;
163
3/3
✓ Branch 72 → 73 taken 1089 times.
✓ Branch 73 → 28 taken 214 times.
✓ Branch 73 → 74 taken 875 times.
1089 while (top.next()) {
164
2/2
✓ Branch 28 → 29 taken 214 times.
✓ Branch 29 → 30 taken 214 times.
214 const QString key = top.value(0).toString();
165
3/3
✓ Branch 31 → 32 taken 214 times.
✓ Branch 32 → 33 taken 214 times.
✓ Branch 33 → 34 taken 214 times.
214 usage.insert(key, top.value(1).toInt());
166
1/1
✓ Branch 35 → 36 taken 214 times.
214 QSqlQuery card(db);
167
1/1
✓ Branch 42 → 43 taken 214 times.
642 card.prepare(QStringLiteral("SELECT %1 FROM card_addresses a JOIN cards c ON c.id = a.card_id "
168 "JOIN address_books b ON b.id = c.book_id WHERE a.address_key = ? AND b.show_in_all = 1 "
169 "ORDER BY (c.sync_state = 'legacy'), c.id LIMIT 1")
170
1/1
✓ Branch 41 → 42 taken 214 times.
642 .arg(QLatin1String(kRowColumns)));
171
2/2
✓ Branch 48 → 49 taken 214 times.
✓ Branch 49 → 50 taken 214 times.
214 card.addBindValue(sqlValue(key));
172
7/8
✓ Branch 52 → 53 taken 214 times.
✓ Branch 53 → 54 taken 214 times.
✗ Branch 53 → 57 not taken.
✓ Branch 54 → 55 taken 214 times.
✓ Branch 55 → 56 taken 97 times.
✓ Branch 55 → 57 taken 117 times.
✓ Branch 58 → 59 taken 97 times.
✓ Branch 58 → 67 taken 117 times.
214 if (card.exec() && card.next()) {
173
1/1
✓ Branch 59 → 60 taken 97 times.
97 Row row = rowFrom(card, false);
174 97 row.useCount = usage.value(key);
175
1/2
✓ Branch 62 → 63 taken 97 times.
✗ Branch 62 → 65 not taken.
97 if (!seenCards.contains(row.key)) {
176
1/1
✓ Branch 63 → 64 taken 97 times.
97 seenCards.insert(row.key);
177
1/1
✓ Branch 64 → 65 taken 97 times.
97 rows.append(row);
178 }
179 97 } else {
180
1/1
✓ Branch 67 → 68 taken 117 times.
117 collectedKeys.insert(key);
181 }
182 214 }
183
2/2
✓ Branch 75 → 76 taken 55 times.
✓ Branch 75 → 84 taken 820 times.
875 if (!collectedKeys.isEmpty())
184
3/3
✓ Branch 77 → 78 taken 55 times.
✓ Branch 78 → 79 taken 55 times.
✓ Branch 79 → 80 taken 55 times.
55 rows += collectedRows(db, QString(), collectedKeys);
185
1/1
✓ Branch 84 → 85 taken 875 times.
875 const QStringList words = filterWords(filter);
186 875 QList<Row> filtered;
187
4/4
✓ Branch 85 → 86 taken 875 times.
✓ Branch 86 → 87 taken 875 times.
✓ Branch 110 → 88 taken 214 times.
✓ Branch 110 → 111 taken 875 times.
1089 for (const Row &row : rows) {
188
2/2
✓ Branch 93 → 94 taken 214 times.
✓ Branch 94 → 95 taken 214 times.
428 const QString haystack = ContactIndex::foldForSort(row.displayName + QLatin1Char(' ') + row.primaryAddress +
189
3/3
✓ Branch 95 → 96 taken 214 times.
✓ Branch 96 → 97 taken 214 times.
✓ Branch 97 → 98 taken 214 times.
428 QLatin1Char(' ') + row.organization);
190
2/3
✓ Branch 104 → 105 taken 214 times.
✓ Branch 105 → 106 taken 214 times.
✗ Branch 105 → 107 not taken.
214 if (std::all_of(words.cbegin(), words.cend(), [&](const QString &w) { return haystack.contains(w); }))
191
1/1
✓ Branch 106 → 107 taken 214 times.
214 filtered.append(row);
192 214 }
193
3/3
✓ Branch 111 → 112 taken 875 times.
✓ Branch 112 → 113 taken 875 times.
✓ Branch 113 → 114 taken 875 times.
875 std::sort(filtered.begin(), filtered.end(), [](const Row &a, const Row &b) {
194
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 283 times.
283 return a.sortKey == b.sortKey ? a.key < b.key : sortKeyLess(a.sortKey, b.sortKey);
195 });
196 875 return filtered;
197 875 }
198
199 7 QList<Row> sliceAfter(const QList<Row> &rows, int limit, const QString &afterKey, qint64 afterId,
200 const QString &afterRowKey, bool keyed) {
201 7 QList<Row> result;
202
2/2
✓ Branch 27 → 4 taken 207 times.
✓ Branch 27 → 28 taken 6 times.
213 for (const Row &row : rows) {
203
2/2
✓ Branch 5 → 6 taken 105 times.
✓ Branch 5 → 21 taken 102 times.
207 if (keyed) {
204
2/3
✓ Branch 6 → 7 taken 105 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 105 times.
105 if (sortKeyLess(row.sortKey, afterKey))
205 ✗ continue;
206
1/2
✓ Branch 10 → 11 taken 105 times.
✗ Branch 10 → 21 not taken.
105 if (row.sortKey == afterKey) {
207 // F-61 (SPRINT-104): collected rows have cardId == 0 — the former
208 // cardId cursor excluded every later row with an equal sort key,
209 // so equal-name collected addresses were skipped at the page
210 // boundary. They tie-break by their stable key instead; card rows
211 // keep the id comparison.
212
2/4
✓ Branch 11 → 12 taken 105 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 16 taken 105 times.
105 if (row.cardId > 0 || afterId > 0) {
213 ✗ if (row.cardId <= afterId)
214 ✗ continue;
215
2/2
✓ Branch 18 → 19 taken 100 times.
✓ Branch 18 → 20 taken 5 times.
105 } else if (row.key <= afterRowKey) {
216 100 continue;
217 }
218 }
219 }
220
1/1
✓ Branch 21 → 22 taken 107 times.
107 result.append(row);
221
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 106 times.
107 if (result.size() >= limit)
222 1 break;
223 }
224 7 return result;
225 ✗ }
226
227 } // namespace
228
229
3/3
✓ Branch 4 → 5 taken 19833 times.
✓ Branch 5 → 6 taken 19833 times.
✓ Branch 6 → 7 taken 19833 times.
19833 QString cardKey(qint64 bookId, const QString &uid) { return QString::number(bookId) + QLatin1Char('/') + uid; }
230
231 2161 bool splitKey(const QString &key, qint64 *bookId, QString *uid) {
232 2161 const int slash = key.indexOf(QLatin1Char('/'));
233
6/7
✓ Branch 5 → 6 taken 2107 times.
✓ Branch 5 → 9 taken 54 times.
✓ Branch 7 → 8 taken 2107 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2107 times.
✓ Branch 11 → 12 taken 54 times.
✓ Branch 11 → 13 taken 2107 times.
2161 if (slash <= 0 || key.startsWith(QLatin1String("collected:")))
234 54 return false;
235 2107 bool ok = false;
236
2/2
✓ Branch 13 → 14 taken 2107 times.
✓ Branch 14 → 15 taken 2107 times.
2107 const qint64 book = key.left(slash).toLongLong(&ok);
237
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 2107 times.
2107 if (!ok)
238 ✗ return false;
239
1/2
✓ Branch 18 → 19 taken 2107 times.
✗ Branch 18 → 20 not taken.
2107 if (bookId)
240 2107 *bookId = book;
241
1/2
✓ Branch 20 → 21 taken 2107 times.
✗ Branch 20 → 25 not taken.
2107 if (uid)
242
1/1
✓ Branch 21 → 22 taken 2107 times.
2107 *uid = key.mid(slash + 1);
243 2107 return true;
244 }
245
246 978 int count(QSqlDatabase db, const View &view, const QString &filter) {
247
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 12 taken 977 times.
978 if (view.kind == ViewKind::Collected)
248
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
1 return int(collectedRows(db, filter).size());
249
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 20 taken 976 times.
977 if (view.kind == ViewKind::Frequent)
250
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
1 return int(frequentRows(db, filter).size());
251 976 QVariantMap binds;
252
1/1
✓ Branch 20 → 21 taken 976 times.
976 QString where = viewCondition(view, &binds);
253
1/1
✓ Branch 21 → 22 taken 976 times.
976 const QString words = filterCondition(filter, &binds);
254
2/2
✓ Branch 23 → 24 taken 4 times.
✓ Branch 23 → 33 taken 972 times.
976 if (!words.isEmpty())
255
2/2
✓ Branch 27 → 28 taken 4 times.
✓ Branch 28 → 29 taken 4 times.
4 where += QStringLiteral(" AND ") + words;
256
1/1
✓ Branch 33 → 34 taken 976 times.
976 QSqlQuery q(db);
257
2/2
✓ Branch 37 → 38 taken 976 times.
✓ Branch 38 → 39 taken 976 times.
976 q.prepare(QStringLiteral("SELECT COUNT(*) FROM cards c JOIN address_books b ON b.id = c.book_id WHERE ") + where);
258
1/1
✓ Branch 42 → 43 taken 976 times.
976 bindAll(&q, binds);
259
9/12
✓ Branch 43 → 44 taken 976 times.
✓ Branch 44 → 45 taken 975 times.
✓ Branch 44 → 50 taken 1 time.
✓ Branch 45 → 46 taken 975 times.
✓ Branch 46 → 47 taken 975 times.
✗ Branch 46 → 50 not taken.
✓ Branch 47 → 48 taken 975 times.
✓ Branch 48 → 49 taken 975 times.
✓ Branch 52 → 53 taken 975 times.
✓ Branch 52 → 54 taken 1 time.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
976 return q.exec() && q.next() ? q.value(0).toInt() : 0;
260 976 }
261
262 1088 QList<Row> page(QSqlDatabase db, const View &view, const QString &filter, int limit, const QString &afterKey,
263 qint64 afterId, const QString &afterRowKey) {
264
4/4
✓ Branch 2 → 3 taken 1074 times.
✓ Branch 2 → 5 taken 14 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1073 times.
1088 const bool keyed = afterId > 0 || !afterKey.isEmpty();
265
2/2
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 17 taken 1081 times.
1088 if (view.kind == ViewKind::Collected)
266
3/3
✓ Branch 9 → 10 taken 7 times.
✓ Branch 10 → 11 taken 7 times.
✓ Branch 11 → 12 taken 7 times.
14 return sliceAfter(collectedRows(db, filter), limit, afterKey, afterId, afterRowKey, keyed);
267
2/2
✓ Branch 17 → 18 taken 54 times.
✓ Branch 17 → 29 taken 1027 times.
1081 if (view.kind == ViewKind::Frequent)
268
6/13
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 54 times.
✓ Branch 20 → 21 taken 54 times.
✓ Branch 21 → 22 taken 54 times.
✓ Branch 22 → 23 taken 54 times.
✓ Branch 24 → 25 taken 54 times.
✗ Branch 24 → 26 not taken.
✓ Branch 26 → 27 taken 54 times.
✗ Branch 26 → 28 not taken.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
108 return keyed ? QList<Row>{} : frequentRows(db, filter).mid(0, limit);
269 1027 QVariantMap binds;
270
1/1
✓ Branch 29 → 30 taken 1027 times.
1027 QString where = viewCondition(view, &binds);
271
1/1
✓ Branch 30 → 31 taken 1027 times.
1027 const QString words = filterCondition(filter, &binds);
272
2/2
✓ Branch 32 → 33 taken 113 times.
✓ Branch 32 → 42 taken 914 times.
1027 if (!words.isEmpty())
273
2/2
✓ Branch 36 → 37 taken 113 times.
✓ Branch 37 → 38 taken 113 times.
113 where += QStringLiteral(" AND ") + words;
274
6/6
✓ Branch 42 → 43 taken 9 times.
✓ Branch 42 → 46 taken 1018 times.
✓ Branch 49 → 50 taken 1018 times.
✓ Branch 49 → 51 taken 9 times.
✓ Branch 51 → 52 taken 9 times.
✓ Branch 51 → 53 taken 1018 times.
2054 const QString sortColumn = view.byFamilyName ? QStringLiteral("c.sort_family") : QStringLiteral("c.sort_display");
275
2/2
✓ Branch 53 → 54 taken 14 times.
✓ Branch 53 → 80 taken 1013 times.
1027 if (keyed) {
276
2/2
✓ Branch 58 → 59 taken 14 times.
✓ Branch 59 → 60 taken 14 times.
28 where += QStringLiteral(" AND (%1 > :afterKey OR (%1 = :afterKey AND c.id > :afterId))").arg(sortColumn);
277
1/1
✓ Branch 67 → 68 taken 14 times.
28 binds.insert(QStringLiteral(":afterKey"), afterKey);
278
1/1
✓ Branch 75 → 76 taken 14 times.
28 binds.insert(QStringLiteral(":afterId"), afterId);
279 }
280 // A search matches few cards: one table scan and sorting the matches is
281 // cheaper than walking the sort index with a row read per entry ("+"
282 // keeps SQLite off the index). Without a search the index walk stops
283 // after `limit` rows (G10, A/B in SPRINT-97 §16.5).
284
3/3
✓ Branch 81 → 82 taken 914 times.
✓ Branch 81 → 83 taken 113 times.
✓ Branch 85 → 86 taken 113 times.
1027 const QString order = words.isEmpty() ? sortColumn : QLatin1Char('+') + sortColumn;
285
1/1
✓ Branch 86 → 87 taken 1027 times.
1027 QSqlQuery q(db);
286
1/1
✓ Branch 92 → 93 taken 1027 times.
3081 q.prepare(QStringLiteral("SELECT %1 FROM cards c JOIN address_books b ON b.id = c.book_id WHERE %2 "
287 "ORDER BY %3, c.id LIMIT :limit")
288
1/1
✓ Branch 91 → 92 taken 1027 times.
2054 .arg(QLatin1String(kRowColumns), where, order));
289
1/1
✓ Branch 96 → 97 taken 1027 times.
1027 bindAll(&q, binds);
290
2/2
✓ Branch 99 → 100 taken 1027 times.
✓ Branch 103 → 104 taken 1027 times.
2054 q.bindValue(QStringLiteral(":limit"), sqlValue(limit));
291 1027 QList<Row> rows;
292
3/3
✓ Branch 108 → 109 taken 1027 times.
✓ Branch 109 → 110 taken 1026 times.
✓ Branch 109 → 117 taken 1 time.
1027 if (q.exec())
293
3/3
✓ Branch 115 → 116 taken 6405 times.
✓ Branch 116 → 111 taken 5379 times.
✓ Branch 116 → 117 taken 1026 times.
6405 while (q.next())
294
2/2
✓ Branch 111 → 112 taken 5379 times.
✓ Branch 112 → 113 taken 5379 times.
5379 rows.append(rowFrom(q, view.byFamilyName));
295 1027 return rows;
296 1027 }
297
298 36 QList<Row> rowsForCards(QSqlDatabase db, const View &view, const QString &filter, const QList<qint64> &cardIds) {
299 36 QList<Row> rows;
300
3/6
✓ Branch 3 → 4 taken 36 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 36 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 36 times.
36 if (cardIds.isEmpty() || view.kind == ViewKind::Collected)
301 ✗ return rows;
302
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 29 taken 36 times.
36 if (view.kind == ViewKind::Frequent) {
303 ✗ const QSet<qint64> wanted(cardIds.cbegin(), cardIds.cend());
304 ✗ for (const Row &row : frequentRows(db, filter))
305 ✗ if (wanted.contains(row.cardId))
306 ✗ rows.append(row);
307 ✗ return rows;
308 ✗ }
309 36 QVariantMap binds;
310
1/1
✓ Branch 29 → 30 taken 36 times.
36 QString where = viewCondition(view, &binds);
311
1/1
✓ Branch 30 → 31 taken 36 times.
36 const QString words = filterCondition(filter, &binds);
312
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 42 taken 36 times.
36 if (!words.isEmpty())
313 ✗ where += QStringLiteral(" AND ") + words;
314 36 QStringList ids;
315
2/2
✓ Branch 50 → 44 taken 36 times.
✓ Branch 50 → 51 taken 36 times.
72 for (qint64 id : cardIds)
316
2/2
✓ Branch 45 → 46 taken 36 times.
✓ Branch 46 → 47 taken 36 times.
36 ids << QString::number(id);
317
1/1
✓ Branch 51 → 52 taken 36 times.
36 QSqlQuery q(db);
318
1/1
✓ Branch 60 → 61 taken 36 times.
108 q.prepare(QStringLiteral("SELECT %1 FROM cards c JOIN address_books b ON b.id = c.book_id WHERE %2 AND c.id IN (%3)")
319
2/2
✓ Branch 57 → 58 taken 36 times.
✓ Branch 59 → 60 taken 36 times.
72 .arg(QLatin1String(kRowColumns), where, ids.join(QLatin1Char(','))));
320
1/1
✓ Branch 65 → 66 taken 36 times.
36 bindAll(&q, binds);
321
2/3
✓ Branch 66 → 67 taken 36 times.
✓ Branch 67 → 68 taken 36 times.
✗ Branch 67 → 75 not taken.
36 if (q.exec())
322
3/3
✓ Branch 73 → 74 taken 72 times.
✓ Branch 74 → 69 taken 36 times.
✓ Branch 74 → 75 taken 36 times.
72 while (q.next())
323
2/2
✓ Branch 69 → 70 taken 36 times.
✓ Branch 70 → 71 taken 36 times.
36 rows.append(rowFrom(q, view.byFamilyName));
324 36 return rows;
325 36 }
326
327 2642 QHash<qint64, int> bookCounts(QSqlDatabase db) {
328 2642 QHash<qint64, int> counts;
329
1/1
✓ Branch 2 → 3 taken 2642 times.
2642 QSqlQuery q(db);
330
2/3
✓ Branch 6 → 7 taken 2642 times.
✓ Branch 9 → 10 taken 2642 times.
✗ Branch 9 → 21 not taken.
2642 if (q.exec(QStringLiteral("SELECT book_id, COUNT(*) FROM cards WHERE kind <> 'group' GROUP BY book_id")))
331
3/3
✓ Branch 19 → 20 taken 4016 times.
✓ Branch 20 → 11 taken 1374 times.
✓ Branch 20 → 21 taken 2642 times.
4016 while (q.next())
332
5/5
✓ Branch 11 → 12 taken 1374 times.
✓ Branch 12 → 13 taken 1374 times.
✓ Branch 13 → 14 taken 1374 times.
✓ Branch 14 → 15 taken 1374 times.
✓ Branch 15 → 16 taken 1374 times.
1374 counts.insert(q.value(0).toLongLong(), q.value(1).toInt());
333 2642 return counts;
334 2642 }
335
336 821 QHash<qint64, int> ungroupedCounts(QSqlDatabase db) {
337 821 QHash<qint64, int> counts;
338
1/1
✓ Branch 2 → 3 taken 821 times.
821 QSqlQuery q(db);
339
1/1
✓ Branch 9 → 10 taken 821 times.
2463 if (q.exec(QStringLiteral("SELECT c.book_id, COUNT(*) FROM cards c WHERE c.kind <> 'group' AND %1 GROUP BY c.book_id")
340
2/3
✓ Branch 8 → 9 taken 821 times.
✓ Branch 13 → 14 taken 821 times.
✗ Branch 13 → 25 not taken.
2463 .arg(QLatin1String(kUngrouped))))
341
3/3
✓ Branch 23 → 24 taken 1121 times.
✓ Branch 24 → 15 taken 300 times.
✓ Branch 24 → 25 taken 821 times.
1121 while (q.next())
342
5/5
✓ Branch 15 → 16 taken 300 times.
✓ Branch 16 → 17 taken 300 times.
✓ Branch 17 → 18 taken 300 times.
✓ Branch 18 → 19 taken 300 times.
✓ Branch 19 → 20 taken 300 times.
300 counts.insert(q.value(0).toLongLong(), q.value(1).toInt());
343 821 return counts;
344 821 }
345
346
2/2
✓ Branch 3 → 4 taken 902 times.
✓ Branch 4 → 5 taken 902 times.
902 int allCount(QSqlDatabase db) { return count(db, View{}, QString()); }
347
348
2/2
✓ Branch 3 → 4 taken 820 times.
✓ Branch 4 → 5 taken 820 times.
820 int frequentCount(QSqlDatabase db) { return int(frequentRows(db, QString()).size()); }
349
350 827 int collectedCount(QSqlDatabase db) {
351
1/1
✓ Branch 2 → 3 taken 827 times.
827 QSqlQuery q(db);
352
2/8
✓ Branch 14 → 15 taken 827 times.
✗ Branch 14 → 16 not taken.
✓ Branch 16 → 17 taken 827 times.
✗ Branch 16 → 22 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
1654 if (q.exec(QStringLiteral("SELECT COUNT(*) FROM collected_addresses WHERE address_key NOT IN "
353
3/5
✓ Branch 6 → 7 taken 827 times.
✓ Branch 7 → 8 taken 827 times.
✗ Branch 7 → 11 not taken.
✓ Branch 12 → 13 taken 827 times.
✗ Branch 12 → 14 not taken.
2481 "(SELECT address_key FROM card_addresses)")) &&
354
2/3
✓ Branch 8 → 9 taken 827 times.
✓ Branch 9 → 10 taken 827 times.
✗ Branch 9 → 11 not taken.
827 q.next())
355
2/2
✓ Branch 17 → 18 taken 827 times.
✓ Branch 18 → 19 taken 827 times.
827 return q.value(0).toInt();
356 ✗ return 0;
357 827 }
358
359 // Recipients match by name (the folded display name) or address — not by
360 // notes, organisation or postal address, which the list search covers
361 // (G10: a short column per row, A/B in SPRINT-97 §16.5).
362 28 QList<Completion> completions(QSqlDatabase db, const QString &fragment, int limit) {
363 28 QList<Completion> result;
364
1/1
✓ Branch 2 → 3 taken 28 times.
28 const QString folded = ContactIndex::foldForSort(fragment);
365
2/2
✓ Branch 3 → 4 taken 28 times.
✓ Branch 4 → 5 taken 28 times.
28 const QString lower = fragment.trimmed().toLower();
366
2/6
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 11 taken 28 times.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 28 times.
28 if (folded.isEmpty() && lower.isEmpty())
367 ✗ return result;
368 28 QSet<QString> seen;
369
1/1
✓ Branch 16 → 17 taken 28 times.
28 QSqlQuery q(db);
370
1/1
✓ Branch 20 → 21 taken 28 times.
28 q.prepare(QStringLiteral(
371 "SELECT c.book_id, c.uid, c.display_name, a.address, a.address_key, b.display_name, "
372 "COALESCE(u.use_count, 0), (b.account_id = '' AND b.path = 'local') "
373 "FROM card_addresses a JOIN cards c ON c.id = a.card_id JOIN address_books b ON b.id = c.book_id "
374 "LEFT JOIN address_usage u ON u.address_key = a.address_key "
375 "WHERE b.use_for_completion = 1 AND (c.sort_display LIKE :name ESCAPE '\\' OR a.address_key LIKE :address ESCAPE '\\') "
376 "ORDER BY COALESCE(u.use_count, 0) DESC, (c.sort_display LIKE :prefix ESCAPE '\\') DESC, c.sort_display, a.position "
377 "LIMIT :limit"));
378
3/3
✓ Branch 24 → 25 taken 28 times.
✓ Branch 26 → 27 taken 28 times.
✓ Branch 30 → 31 taken 28 times.
56 q.bindValue(QStringLiteral(":name"), sqlValue(likePattern(folded)));
379
3/3
✓ Branch 37 → 38 taken 28 times.
✓ Branch 39 → 40 taken 28 times.
✓ Branch 43 → 44 taken 28 times.
56 q.bindValue(QStringLiteral(":address"), sqlValue(likePattern(lower)));
380
4/4
✓ Branch 50 → 51 taken 28 times.
✓ Branch 51 → 52 taken 28 times.
✓ Branch 53 → 54 taken 28 times.
✓ Branch 57 → 58 taken 28 times.
56 q.bindValue(QStringLiteral(":prefix"), sqlValue(likePattern(folded).mid(1)));
381
2/2
✓ Branch 66 → 67 taken 28 times.
✓ Branch 70 → 71 taken 28 times.
56 q.bindValue(QStringLiteral(":limit"), sqlValue(limit * 3));
382
3/3
✓ Branch 75 → 76 taken 28 times.
✓ Branch 76 → 77 taken 27 times.
✓ Branch 76 → 134 taken 1 time.
28 if (q.exec()) {
383
6/7
✓ Branch 127 → 128 taken 42 times.
✓ Branch 128 → 129 taken 15 times.
✓ Branch 128 → 132 taken 27 times.
✓ Branch 130 → 131 taken 15 times.
✗ Branch 130 → 132 not taken.
✓ Branch 133 → 78 taken 15 times.
✓ Branch 133 → 134 taken 27 times.
42 while (q.next() && result.size() < limit) {
384
2/2
✓ Branch 78 → 79 taken 15 times.
✓ Branch 79 → 80 taken 15 times.
15 const QString key = q.value(4).toString();
385
1/2
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 84 taken 15 times.
15 if (seen.contains(key))
386 ✗ continue;
387
1/1
✓ Branch 84 → 85 taken 15 times.
15 seen.insert(key);
388 15 Completion completion;
389
5/5
✓ Branch 85 → 86 taken 15 times.
✓ Branch 86 → 87 taken 15 times.
✓ Branch 87 → 88 taken 15 times.
✓ Branch 88 → 89 taken 15 times.
✓ Branch 89 → 90 taken 15 times.
15 completion.key = cardKey(q.value(0).toLongLong(), q.value(1).toString());
390
2/2
✓ Branch 95 → 96 taken 15 times.
✓ Branch 96 → 97 taken 15 times.
15 completion.uid = q.value(1).toString();
391
2/2
✓ Branch 100 → 101 taken 15 times.
✓ Branch 101 → 102 taken 15 times.
15 completion.name = q.value(2).toString();
392
2/2
✓ Branch 105 → 106 taken 15 times.
✓ Branch 106 → 107 taken 15 times.
15 completion.address = q.value(3).toString();
393
2/2
✓ Branch 110 → 111 taken 15 times.
✓ Branch 111 → 112 taken 15 times.
15 completion.bookName = q.value(5).toString();
394
2/2
✓ Branch 115 → 116 taken 15 times.
✓ Branch 116 → 117 taken 15 times.
15 completion.useCount = q.value(6).toInt();
395
1/1
✓ Branch 118 → 119 taken 15 times.
15 completion.avatarColor = ContactIndex::avatarColorIndex(completion.uid);
396
1/1
✓ Branch 119 → 120 taken 15 times.
15 result.append(completion);
397 15 }
398 }
399
1/1
✓ Branch 134 → 135 taken 28 times.
28 QSqlQuery collected(db);
400
1/1
✓ Branch 138 → 139 taken 28 times.
28 collected.prepare(QStringLiteral(
401 "SELECT ca.address, ca.address_key, ca.display_name, COALESCE(u.use_count, 0) FROM collected_addresses ca "
402 "LEFT JOIN address_usage u ON u.address_key = ca.address_key "
403 "WHERE (ca.address_key LIKE :address ESCAPE '\\' OR lower(ca.display_name) LIKE :address ESCAPE '\\') "
404 "ORDER BY COALESCE(u.use_count, 0) DESC LIMIT :limit"));
405
3/3
✓ Branch 142 → 143 taken 28 times.
✓ Branch 144 → 145 taken 28 times.
✓ Branch 148 → 149 taken 28 times.
56 collected.bindValue(QStringLiteral(":address"), sqlValue(likePattern(lower)));
406
2/2
✓ Branch 156 → 157 taken 28 times.
✓ Branch 160 → 161 taken 28 times.
56 collected.bindValue(QStringLiteral(":limit"), sqlValue(limit));
407 28 QList<Completion> extra;
408
3/3
✓ Branch 165 → 166 taken 28 times.
✓ Branch 166 → 167 taken 27 times.
✓ Branch 166 → 207 taken 1 time.
28 if (collected.exec()) {
409
3/3
✓ Branch 205 → 206 taken 31 times.
✓ Branch 206 → 168 taken 4 times.
✓ Branch 206 → 207 taken 27 times.
31 while (collected.next()) {
410
2/2
✓ Branch 168 → 169 taken 4 times.
✓ Branch 169 → 170 taken 4 times.
4 const QString key = collected.value(1).toString();
411
1/2
✗ Branch 172 → 173 not taken.
✓ Branch 172 → 174 taken 4 times.
4 if (seen.contains(key))
412 ✗ continue;
413
1/1
✓ Branch 174 → 175 taken 4 times.
4 seen.insert(key);
414 4 Completion completion;
415
1/1
✓ Branch 178 → 179 taken 4 times.
4 completion.key = QStringLiteral("collected:") + key;
416
2/2
✓ Branch 183 → 184 taken 4 times.
✓ Branch 184 → 185 taken 4 times.
4 completion.address = collected.value(0).toString();
417
2/2
✓ Branch 188 → 189 taken 4 times.
✓ Branch 189 → 190 taken 4 times.
4 completion.name = collected.value(2).toString();
418 4 completion.collected = true;
419
2/2
✓ Branch 193 → 194 taken 4 times.
✓ Branch 194 → 195 taken 4 times.
4 completion.useCount = collected.value(3).toInt();
420
1/1
✓ Branch 196 → 197 taken 4 times.
4 completion.avatarColor = ContactIndex::avatarColorIndex(key);
421
1/1
✓ Branch 197 → 198 taken 4 times.
4 extra.append(completion);
422 4 }
423 }
424 // Usage ranks across both sources.
425
1/1
✓ Branch 207 → 208 taken 28 times.
28 result += extra;
426
3/3
✓ Branch 208 → 209 taken 28 times.
✓ Branch 209 → 210 taken 28 times.
✓ Branch 210 → 211 taken 28 times.
28 std::stable_sort(result.begin(), result.end(),
427 3 [](const Completion &a, const Completion &b) { return a.useCount > b.useCount; });
428 // S97.5.4: groups by name prefix, ahead of single addresses.
429 28 QList<Completion> groupsFound;
430
6/6
✓ Branch 211 → 212 taken 28 times.
✓ Branch 212 → 213 taken 28 times.
✓ Branch 214 → 215 taken 28 times.
✓ Branch 215 → 216 taken 28 times.
✓ Branch 284 → 217 taken 12 times.
✓ Branch 284 → 285 taken 28 times.
40 for (const Group &group : groups(db)) {
431
8/12
✓ Branch 218 → 219 taken 12 times.
✓ Branch 219 → 220 taken 12 times.
✓ Branch 220 → 221 taken 8 times.
✓ Branch 220 → 223 taken 4 times.
✗ Branch 222 → 223 not taken.
✓ Branch 222 → 224 taken 8 times.
✓ Branch 225 → 226 taken 12 times.
✗ Branch 225 → 227 not taken.
✓ Branch 227 → 228 taken 4 times.
✓ Branch 227 → 229 taken 8 times.
✗ Branch 476 → 477 not taken.
✗ Branch 476 → 478 not taken.
12 if (!ContactIndex::foldForSort(group.name).startsWith(folded) || folded.isEmpty())
432 4 continue;
433
1/1
✓ Branch 229 → 230 taken 8 times.
8 QSqlQuery book(db);
434
1/1
✓ Branch 233 → 234 taken 8 times.
8 book.prepare(QStringLiteral("SELECT display_name FROM address_books WHERE id = ? AND use_for_completion = 1"));
435
1/1
✓ Branch 238 → 239 taken 8 times.
8 book.addBindValue(group.bookId);
436
5/8
✓ Branch 240 → 241 taken 8 times.
✓ Branch 241 → 242 taken 8 times.
✗ Branch 241 → 244 not taken.
✓ Branch 242 → 243 taken 8 times.
✗ Branch 243 → 244 not taken.
✓ Branch 243 → 245 taken 8 times.
✗ Branch 246 → 247 not taken.
✓ Branch 246 → 248 taken 8 times.
8 if (!book.exec() || !book.next())
437 ✗ continue;
438 8 Completion completion;
439
2/2
✓ Branch 252 → 253 taken 8 times.
✓ Branch 254 → 255 taken 8 times.
24 completion.key = QStringLiteral("group:%1/%2").arg(group.bookId).arg(group.name);
440 8 completion.name = group.name;
441
2/2
✓ Branch 261 → 262 taken 8 times.
✓ Branch 262 → 263 taken 8 times.
8 completion.bookName = book.value(0).toString();
442 8 completion.group = true;
443
2/2
✓ Branch 266 → 267 taken 8 times.
✓ Branch 267 → 268 taken 8 times.
8 completion.members = groupMembers(db, group.bookId, group.name);
444
1/1
✓ Branch 271 → 272 taken 8 times.
8 completion.avatarColor = ContactIndex::avatarColorIndex(completion.key);
445
1/2
✓ Branch 273 → 274 taken 8 times.
✗ Branch 273 → 275 not taken.
8 if (!completion.members.isEmpty())
446
1/1
✓ Branch 274 → 275 taken 8 times.
8 groupsFound.append(completion);
447 36 }
448
2/2
✓ Branch 286 → 287 taken 28 times.
✓ Branch 287 → 288 taken 28 times.
28 return (groupsFound + result).mid(0, limit);
449 28 }
450
451 104 QList<Duplicate> duplicatesOf(QSqlDatabase db, const QString &key) {
452 104 QList<Duplicate> result;
453 104 qint64 bookId = 0;
454 104 QString uid;
455
2/3
✓ Branch 2 → 3 taken 104 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 104 times.
104 if (!splitKey(key, &bookId, &uid))
456 ✗ return result;
457
1/1
✓ Branch 5 → 6 taken 104 times.
104 QSqlQuery q(db);
458
1/1
✓ Branch 9 → 10 taken 104 times.
104 q.prepare(QStringLiteral(
459 "SELECT DISTINCT o.book_id, o.uid, o.display_name, b.display_name, (b.account_id = '' AND b.path = 'local') "
460 "FROM cards c JOIN cards o ON o.id <> c.id AND o.kind NOT IN ('group', 'family') "
461 "JOIN address_books b ON b.id = o.book_id "
462 "WHERE c.book_id = :book AND c.uid = :uid AND c.kind NOT IN ('group', 'family') AND ("
463 " (c.sort_display <> '' AND o.sort_display = c.sort_display) OR"
464 " EXISTS (SELECT 1 FROM card_addresses a JOIN card_addresses oa ON oa.address_key = a.address_key"
465 " WHERE a.card_id = c.id AND oa.card_id = o.id)) "
466 "ORDER BY (o.book_id = :book) DESC, o.sort_display LIMIT 10"));
467
1/1
✓ Branch 17 → 18 taken 104 times.
208 q.bindValue(QStringLiteral(":book"), bookId);
468
2/2
✓ Branch 23 → 24 taken 104 times.
✓ Branch 27 → 28 taken 104 times.
208 q.bindValue(QStringLiteral(":uid"), sqlValue(uid));
469
2/3
✓ Branch 32 → 33 taken 104 times.
✓ Branch 33 → 34 taken 104 times.
✗ Branch 33 → 63 not taken.
104 if (q.exec())
470
3/3
✓ Branch 61 → 62 taken 106 times.
✓ Branch 62 → 35 taken 2 times.
✓ Branch 62 → 63 taken 104 times.
106 while (q.next())
471
3/3
✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
6 result.append({cardKey(q.value(0).toLongLong(), q.value(1).toString()), q.value(2).toString(),
472
4/7
✓ Branch 43 → 44 taken 2 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 49 → 50 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 53 → 55 not taken.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
8 q.value(4).toBool() ? QString() : q.value(3).toString(), q.value(0).toLongLong() == bookId});
473 104 return result;
474 112 }
475
476 1327 Lookup lookup(QSqlDatabase db, const QString &address) {
477 1327 Lookup result;
478
1/1
✓ Branch 2 → 3 taken 1327 times.
1327 const QString key = ContactIndex::addressKey(address);
479
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1327 times.
1327 if (key.isEmpty())
480 ✗ return result;
481
1/1
✓ Branch 6 → 7 taken 1327 times.
1327 QSqlQuery q(db);
482
1/1
✓ Branch 10 → 11 taken 1327 times.
1327 q.prepare(QStringLiteral("SELECT c.book_id, c.uid, c.display_name, b.display_name FROM card_addresses a "
483 "JOIN cards c ON c.id = a.card_id JOIN address_books b ON b.id = c.book_id "
484 "WHERE a.address_key = ? ORDER BY (c.sync_state = 'legacy'), b.show_in_all DESC, c.id LIMIT 1"));
485
2/2
✓ Branch 15 → 16 taken 1327 times.
✓ Branch 16 → 17 taken 1327 times.
1327 q.addBindValue(sqlValue(key));
486
7/8
✓ Branch 19 → 20 taken 1327 times.
✓ Branch 20 → 21 taken 1327 times.
✗ Branch 20 → 24 not taken.
✓ Branch 21 → 22 taken 1327 times.
✓ Branch 22 → 23 taken 82 times.
✓ Branch 22 → 24 taken 1245 times.
✓ Branch 25 → 26 taken 82 times.
✓ Branch 25 → 48 taken 1245 times.
1327 if (q.exec() && q.next()) {
487 82 result.known = true;
488
2/2
✓ Branch 26 → 27 taken 82 times.
✓ Branch 27 → 28 taken 82 times.
82 result.bookId = q.value(0).toLongLong();
489
2/2
✓ Branch 29 → 30 taken 82 times.
✓ Branch 30 → 31 taken 82 times.
82 result.uid = q.value(1).toString();
490
1/1
✓ Branch 34 → 35 taken 82 times.
82 result.key = cardKey(result.bookId, result.uid);
491
2/2
✓ Branch 37 → 38 taken 82 times.
✓ Branch 38 → 39 taken 82 times.
82 result.displayName = q.value(2).toString();
492
2/2
✓ Branch 42 → 43 taken 82 times.
✓ Branch 43 → 44 taken 82 times.
82 result.bookName = q.value(3).toString();
493 82 return result;
494 }
495
1/1
✓ Branch 48 → 49 taken 1245 times.
1245 QSqlQuery collected(db);
496
1/1
✓ Branch 52 → 53 taken 1245 times.
1245 collected.prepare(QStringLiteral("SELECT display_name FROM collected_addresses WHERE address_key = ?"));
497
2/2
✓ Branch 57 → 58 taken 1245 times.
✓ Branch 58 → 59 taken 1245 times.
1245 collected.addBindValue(sqlValue(key));
498
7/8
✓ Branch 61 → 62 taken 1245 times.
✓ Branch 62 → 63 taken 1245 times.
✗ Branch 62 → 66 not taken.
✓ Branch 63 → 64 taken 1245 times.
✓ Branch 64 → 65 taken 23 times.
✓ Branch 64 → 66 taken 1222 times.
✓ Branch 67 → 68 taken 23 times.
✓ Branch 67 → 82 taken 1222 times.
1245 if (collected.exec() && collected.next()) {
499 23 result.collected = true;
500
1/1
✓ Branch 71 → 72 taken 23 times.
23 result.key = QStringLiteral("collected:") + key;
501
2/2
✓ Branch 76 → 77 taken 23 times.
✓ Branch 77 → 78 taken 23 times.
23 result.displayName = collected.value(0).toString();
502 }
503 1245 return result;
504 1327 }
505
506 57 QList<Lookup> sameBookOwners(QSqlDatabase db, qint64 bookId, const QString &address, const QString &exceptKey) {
507 57 QList<Lookup> owners;
508
1/1
✓ Branch 2 → 3 taken 57 times.
57 QSqlQuery q(db);
509
1/1
✓ Branch 6 → 7 taken 57 times.
57 q.prepare(QStringLiteral("SELECT c.book_id, c.uid, c.display_name FROM card_addresses a JOIN cards c ON c.id = a.card_id "
510 "WHERE a.address_key = ? AND c.book_id = ? AND c.sync_state <> 'legacy'"));
511
3/3
✓ Branch 10 → 11 taken 57 times.
✓ Branch 12 → 13 taken 57 times.
✓ Branch 13 → 14 taken 57 times.
57 q.addBindValue(sqlValue(ContactIndex::addressKey(address)));
512
2/2
✓ Branch 19 → 20 taken 57 times.
✓ Branch 20 → 21 taken 57 times.
57 q.addBindValue(sqlValue(bookId));
513
2/3
✓ Branch 23 → 24 taken 57 times.
✓ Branch 24 → 25 taken 57 times.
✗ Branch 24 → 49 not taken.
57 if (q.exec()) {
514
3/3
✓ Branch 47 → 48 taken 94 times.
✓ Branch 48 → 26 taken 37 times.
✓ Branch 48 → 49 taken 57 times.
94 while (q.next()) {
515 37 Lookup owner;
516 37 owner.known = true;
517
2/2
✓ Branch 26 → 27 taken 37 times.
✓ Branch 27 → 28 taken 37 times.
37 owner.bookId = q.value(0).toLongLong();
518
2/2
✓ Branch 29 → 30 taken 37 times.
✓ Branch 30 → 31 taken 37 times.
37 owner.uid = q.value(1).toString();
519
1/1
✓ Branch 34 → 35 taken 37 times.
37 owner.key = cardKey(owner.bookId, owner.uid);
520
2/2
✓ Branch 37 → 38 taken 37 times.
✓ Branch 38 → 39 taken 37 times.
37 owner.displayName = q.value(2).toString();
521
2/2
✓ Branch 43 → 44 taken 13 times.
✓ Branch 43 → 45 taken 24 times.
37 if (owner.key != exceptKey)
522
1/1
✓ Branch 44 → 45 taken 13 times.
13 owners.append(owner);
523 37 }
524 }
525 57 return owners;
526 57 }
527
528 3 QStringList cardAddresses(QSqlDatabase db, qint64 cardId) {
529 3 QStringList addresses;
530
1/1
✓ Branch 2 → 3 taken 3 times.
3 QSqlQuery q(db);
531
1/1
✓ Branch 6 → 7 taken 3 times.
3 q.prepare(QStringLiteral("SELECT address FROM card_addresses WHERE card_id = ? ORDER BY preferred DESC, position"));
532
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
3 q.addBindValue(sqlValue(cardId));
533
2/3
✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 26 not taken.
3 if (q.exec())
534
6/6
✓ Branch 18 → 19 taken 6 times.
✓ Branch 19 → 20 taken 6 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 24 → 25 taken 9 times.
✓ Branch 25 → 18 taken 6 times.
✓ Branch 25 → 26 taken 3 times.
9 while (q.next()) addresses.append(q.value(0).toString());
535 3 return addresses;
536 3 }
537
538 } // namespace ContactQueries
539