data/ContactGroupQueries.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Contact groups — CATEGORIES names and group cards (kind = 'group') with | ||
| 2 | // their members. Part of ContactQueries (see ContactQueries.cpp). | ||
| 3 | #include "ContactQueries.h" | ||
| 4 | |||
| 5 | #include "ContactIndex.h" | ||
| 6 | #include "ContactSqlValue.h" | ||
| 7 | #include "MailAddressList.h" | ||
| 8 | |||
| 9 | #include <QSet> | ||
| 10 | #include <QSqlQuery> | ||
| 11 | #include <QUrl> | ||
| 12 | #include <algorithm> | ||
| 13 | |||
| 14 | namespace ContactQueries { | ||
| 15 | |||
| 16 | 857 | QList<Group> groups(QSqlDatabase db, qint64 bookId) { | |
| 17 | 857 | QList<Group> result; | |
| 18 | 857 | QHash<QString, int> index; | |
| 19 |
7/11✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 856 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 856 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 856 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
✗ Branch 119 → 120 not taken.
✗ Branch 119 → 121 not taken.
|
858 | const QString bookFilter = bookId > 0 ? QStringLiteral(" AND c.book_id = %1").arg(bookId) : QString(); |
| 20 |
1/1✓ Branch 13 → 14 taken 857 times.
|
857 | QSqlQuery categories(db); |
| 21 |
1/1✓ Branch 19 → 20 taken 857 times.
|
2571 | categories.exec(QStringLiteral("SELECT c.book_id, g.name, COUNT(DISTINCT c.id) FROM card_groups g " |
| 22 | "JOIN cards c ON c.id = g.card_id WHERE c.kind <> 'group'%1 " | ||
| 23 |
1/1✓ Branch 18 → 19 taken 857 times.
|
1714 | "GROUP BY c.book_id, g.name").arg(bookFilter)); |
| 24 |
3/3✓ Branch 46 → 47 taken 1022 times.
✓ Branch 47 → 24 taken 165 times.
✓ Branch 47 → 48 taken 857 times.
|
1022 | while (categories.next()) { |
| 25 |
6/8✓ Branch 24 → 25 taken 165 times.
✓ Branch 25 → 26 taken 165 times.
✓ Branch 26 → 27 taken 165 times.
✓ Branch 27 → 28 taken 165 times.
✓ Branch 28 → 29 taken 165 times.
✓ Branch 29 → 30 taken 165 times.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
|
165 | Group group{categories.value(0).toLongLong(), categories.value(1).toString(), 0, categories.value(2).toInt()}; |
| 26 |
4/4✓ Branch 36 → 37 taken 165 times.
✓ Branch 37 → 38 taken 165 times.
✓ Branch 38 → 39 taken 165 times.
✓ Branch 39 → 40 taken 165 times.
|
165 | index.insert(QString::number(group.bookId) + QLatin1Char('\n') + group.name, int(result.size())); |
| 27 |
1/1✓ Branch 43 → 44 taken 165 times.
|
165 | result.append(group); |
| 28 | 165 | } | |
| 29 |
1/1✓ Branch 48 → 49 taken 857 times.
|
857 | QSqlQuery cards(db); |
| 30 |
1/1✓ Branch 54 → 55 taken 857 times.
|
2571 | cards.exec(QStringLiteral("SELECT c.book_id, c.display_name, c.id, " |
| 31 | "(SELECT COUNT(*) FROM card_members m WHERE m.card_id = c.id) " | ||
| 32 |
1/1✓ Branch 53 → 54 taken 857 times.
|
1714 | "FROM cards c WHERE c.kind = 'group'%1").arg(bookFilter)); |
| 33 |
3/3✓ Branch 103 → 104 taken 910 times.
✓ Branch 104 → 59 taken 53 times.
✓ Branch 104 → 105 taken 857 times.
|
910 | while (cards.next()) { |
| 34 |
6/6✓ Branch 59 → 60 taken 53 times.
✓ Branch 60 → 61 taken 53 times.
✓ Branch 63 → 64 taken 53 times.
✓ Branch 64 → 65 taken 53 times.
✓ Branch 65 → 66 taken 53 times.
✓ Branch 66 → 67 taken 53 times.
|
53 | const QString key = cards.value(0).toString() + QLatin1Char('\n') + cards.value(1).toString(); |
| 35 |
1/2✗ Branch 73 → 74 not taken.
✓ Branch 73 → 83 taken 53 times.
|
53 | if (index.contains(key)) { |
| 36 | ✗ | Group &existing = result[index.value(key)]; | |
| 37 | ✗ | existing.cardId = cards.value(2).toLongLong(); | |
| 38 | ✗ | existing.memberCount += cards.value(3).toInt(); | |
| 39 | ✗ | continue; | |
| 40 | ✗ | } | |
| 41 |
3/3✓ Branch 83 → 84 taken 53 times.
✓ Branch 84 → 85 taken 53 times.
✓ Branch 88 → 89 taken 53 times.
|
212 | result.append({cards.value(0).toLongLong(), cards.value(1).toString(), cards.value(2).toLongLong(), |
| 42 |
1/1✓ Branch 90 → 91 taken 53 times.
|
106 | cards.value(3).toInt()}); |
| 43 | 53 | } | |
| 44 |
3/3✓ Branch 105 → 106 taken 857 times.
✓ Branch 106 → 107 taken 857 times.
✓ Branch 107 → 108 taken 857 times.
|
857 | std::sort(result.begin(), result.end(), [](const Group &a, const Group &b) { |
| 45 |
6/10✓ Branch 2 → 3 taken 153 times.
✓ Branch 2 → 8 taken 104 times.
✓ Branch 3 → 4 taken 153 times.
✓ Branch 4 → 5 taken 153 times.
✓ Branch 11 → 12 taken 153 times.
✓ Branch 11 → 13 taken 104 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
|
514 | return a.bookId == b.bookId ? ContactIndex::foldForSort(a.name) < ContactIndex::foldForSort(b.name) |
| 46 |
2/2✓ Branch 9 → 10 taken 153 times.
✓ Branch 9 → 11 taken 104 times.
|
514 | : a.bookId < b.bookId; |
| 47 | }); | ||
| 48 | 857 | return result; | |
| 49 | 1016 | } | |
| 50 | |||
| 51 | 25 | QStringList groupMemberKeys(QSqlDatabase db, qint64 bookId, const QString &group) { | |
| 52 |
1/1✓ Branch 2 → 3 taken 25 times.
|
25 | QSqlQuery q(db); |
| 53 |
1/1✓ Branch 6 → 7 taken 25 times.
|
25 | q.prepare(QStringLiteral( |
| 54 | "SELECT c.uid FROM cards c WHERE c.book_id = :book AND c.kind <> 'group' AND (" | ||
| 55 | " EXISTS (SELECT 1 FROM card_groups g WHERE g.card_id = c.id AND g.name = :group) OR" | ||
| 56 | " EXISTS (SELECT 1 FROM cards gc JOIN card_members m ON m.card_id = gc.id WHERE gc.book_id = c.book_id" | ||
| 57 | " AND gc.kind = 'group' AND gc.display_name = :group" | ||
| 58 | " AND (m.member = 'urn:uuid:' || c.uid OR m.member = c.uid)))")); | ||
| 59 |
1/1✓ Branch 14 → 15 taken 25 times.
|
50 | q.bindValue(QStringLiteral(":book"), bookId); |
| 60 |
2/2✓ Branch 20 → 21 taken 25 times.
✓ Branch 24 → 25 taken 25 times.
|
50 | q.bindValue(QStringLiteral(":group"), sqlValue(group)); |
| 61 | 25 | QStringList keys; | |
| 62 |
2/3✓ Branch 29 → 30 taken 25 times.
✓ Branch 30 → 31 taken 25 times.
✗ Branch 30 → 42 not taken.
|
25 | if (q.exec()) |
| 63 |
3/3✓ Branch 40 → 41 taken 99 times.
✓ Branch 41 → 32 taken 74 times.
✓ Branch 41 → 42 taken 25 times.
|
99 | while (q.next()) |
| 64 |
4/4✓ Branch 32 → 33 taken 74 times.
✓ Branch 33 → 34 taken 74 times.
✓ Branch 34 → 35 taken 74 times.
✓ Branch 35 → 36 taken 74 times.
|
74 | keys.append(cardKey(bookId, q.value(0).toString())); |
| 65 | 25 | return keys; | |
| 66 | 25 | } | |
| 67 | |||
| 68 | 2 | QString groupCardKey(QSqlDatabase db, qint64 bookId, const QString &group) { | |
| 69 |
1/1✓ Branch 2 → 3 taken 2 times.
|
2 | QSqlQuery q(db); |
| 70 |
1/1✓ Branch 6 → 7 taken 2 times.
|
2 | q.prepare(QStringLiteral("SELECT uid FROM cards WHERE book_id = ? AND kind = 'group' AND display_name = ? LIMIT 1")); |
| 71 |
1/1✓ Branch 11 → 12 taken 2 times.
|
2 | q.addBindValue(bookId); |
| 72 |
2/2✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
|
2 | q.addBindValue(sqlValue(group)); |
| 73 |
6/17✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 27 not taken.
✓ Branch 21 → 22 taken 2 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 2 times.
✗ Branch 23 → 24 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 2 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 2 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
|
6 | return q.exec() && q.next() ? cardKey(bookId, q.value(0).toString()) : QString(); |
| 74 | 2 | } | |
| 75 | |||
| 76 | 13 | QStringList groupMembers(QSqlDatabase db, qint64 bookId, const QString &group) { | |
| 77 |
1/1✓ Branch 2 → 3 taken 13 times.
|
13 | QSqlQuery q(db); |
| 78 |
1/1✓ Branch 6 → 7 taken 13 times.
|
13 | q.prepare(QStringLiteral( |
| 79 | "SELECT c.display_name, c.primary_address FROM cards c WHERE c.book_id = :book AND c.kind <> 'group' " | ||
| 80 | "AND c.primary_address <> '' AND (" | ||
| 81 | " EXISTS (SELECT 1 FROM card_groups g WHERE g.card_id = c.id AND g.name = :group) OR" | ||
| 82 | " EXISTS (SELECT 1 FROM cards gc JOIN card_members m ON m.card_id = gc.id WHERE gc.book_id = c.book_id" | ||
| 83 | " AND gc.kind = 'group' AND gc.display_name = :group" | ||
| 84 | " AND (m.member = 'urn:uuid:' || c.uid OR m.member = c.uid))) " | ||
| 85 | "ORDER BY c.sort_display")); | ||
| 86 |
1/1✓ Branch 14 → 15 taken 13 times.
|
26 | q.bindValue(QStringLiteral(":book"), bookId); |
| 87 |
2/2✓ Branch 20 → 21 taken 13 times.
✓ Branch 24 → 25 taken 13 times.
|
26 | q.bindValue(QStringLiteral(":group"), sqlValue(group)); |
| 88 | 13 | QStringList members; | |
| 89 | 13 | QSet<QString> seen; | |
| 90 |
2/3✓ Branch 30 → 31 taken 13 times.
✓ Branch 31 → 32 taken 13 times.
✗ Branch 31 → 57 not taken.
|
13 | if (q.exec()) |
| 91 |
3/3✓ Branch 55 → 56 taken 39 times.
✓ Branch 56 → 33 taken 26 times.
✓ Branch 56 → 57 taken 13 times.
|
39 | while (q.next()) { |
| 92 |
2/2✓ Branch 33 → 34 taken 26 times.
✓ Branch 34 → 35 taken 26 times.
|
26 | const QString name = q.value(0).toString(); |
| 93 |
2/2✓ Branch 36 → 37 taken 26 times.
✓ Branch 37 → 38 taken 26 times.
|
26 | const QString address = q.value(1).toString(); |
| 94 | 26 | const QString formatted = MailAddressList::format({name, address}); | |
| 95 |
2/3✓ Branch 43 → 44 taken 26 times.
✓ Branch 46 → 47 taken 26 times.
✗ Branch 46 → 51 not taken.
|
26 | if (!seen.contains(address.toLower())) { |
| 96 |
2/2✓ Branch 47 → 48 taken 26 times.
✓ Branch 48 → 49 taken 26 times.
|
26 | seen.insert(address.toLower()); |
| 97 |
1/1✓ Branch 50 → 51 taken 26 times.
|
26 | members.append(formatted); |
| 98 | } | ||
| 99 | 26 | } | |
| 100 | // F-62 (SPRINT-104): a direct mailto member of the group (MEMBER:mailto: | ||
| 101 | // external@example.test, RFC 6350 §6.6.5) is a valid recipient even | ||
| 102 | // when no local card carries it. The raw MEMBER values stay untouched; | ||
| 103 | // unsupported schemes (tel, xmpp, …) are not email recipients and are | ||
| 104 | // skipped. | ||
| 105 |
1/1✓ Branch 57 → 58 taken 13 times.
|
13 | QSqlQuery mailto(db); |
| 106 |
1/1✓ Branch 61 → 62 taken 13 times.
|
13 | mailto.prepare(QStringLiteral( |
| 107 | "SELECT m.member FROM card_members m JOIN cards gc ON gc.id = m.card_id " | ||
| 108 | "WHERE gc.book_id = :book AND gc.kind = 'group' AND gc.display_name = :group")); | ||
| 109 |
1/1✓ Branch 69 → 70 taken 13 times.
|
26 | mailto.bindValue(QStringLiteral(":book"), bookId); |
| 110 |
2/2✓ Branch 75 → 76 taken 13 times.
✓ Branch 79 → 80 taken 13 times.
|
26 | mailto.bindValue(QStringLiteral(":group"), sqlValue(group)); |
| 111 |
2/3✓ Branch 84 → 85 taken 13 times.
✓ Branch 85 → 86 taken 13 times.
✗ Branch 85 → 136 not taken.
|
13 | if (mailto.exec()) { |
| 112 |
3/3✓ Branch 134 → 135 taken 19 times.
✓ Branch 135 → 87 taken 6 times.
✓ Branch 135 → 136 taken 13 times.
|
19 | while (mailto.next()) { |
| 113 |
3/3✓ Branch 87 → 88 taken 6 times.
✓ Branch 88 → 89 taken 6 times.
✓ Branch 89 → 90 taken 6 times.
|
6 | const QString value = mailto.value(0).toString().trimmed(); |
| 114 |
3/3✓ Branch 95 → 96 taken 6 times.
✓ Branch 98 → 99 taken 2 times.
✓ Branch 98 → 100 taken 4 times.
|
6 | if (!value.startsWith(QStringLiteral("mailto:"), Qt::CaseInsensitive)) |
| 115 | 2 | continue; | |
| 116 | // S106-168: RFC 6068 header fields (?subject=…) are no part of the address. | ||
| 117 | const QString address = | ||
| 118 |
6/6✓ Branch 100 → 101 taken 4 times.
✓ Branch 104 → 105 taken 4 times.
✓ Branch 105 → 106 taken 4 times.
✓ Branch 106 → 107 taken 4 times.
✓ Branch 107 → 108 taken 4 times.
✓ Branch 108 → 109 taken 4 times.
|
8 | QUrl::fromPercentEncoding(value.mid(7).section(QLatin1Char('?'), 0, 0).toUtf8()).trimmed().toLower(); |
| 119 | // A mailto that also resolves to a member card above adds nothing. | ||
| 120 |
5/6✓ Branch 115 → 116 taken 4 times.
✗ Branch 115 → 118 not taken.
✓ Branch 117 → 118 taken 1 time.
✓ Branch 117 → 119 taken 3 times.
✓ Branch 120 → 121 taken 1 time.
✓ Branch 120 → 122 taken 3 times.
|
4 | if (address.isEmpty() || seen.contains(address)) |
| 121 | 1 | continue; | |
| 122 |
1/1✓ Branch 122 → 123 taken 3 times.
|
3 | seen.insert(address); |
| 123 |
1/1✓ Branch 123 → 124 taken 3 times.
|
3 | members.append(address); |
| 124 | 7 | } | |
| 125 | } | ||
| 126 | 13 | return members; | |
| 127 | 39 | } | |
| 128 | |||
| 129 | } // namespace ContactQueries | ||
| 130 |