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: 92.8% 1371 / 0 / 1478
Functions: 100.0% 101 / 0 / 101
Branches: 81.2% 2078 / 31 / 2589

data/CalendarStore.cpp
Line Branch Exec Source
1 #include "CalendarStore.h"
2 #include "CalendarPeopleIndex.h"
3 #include "ICalEventCodec.h"
4 #include "ICalTodoCodec.h"
5 #include <algorithm>
6 #include <QJsonArray>
7 #include <QJsonDocument>
8 #include "DatabaseSecurity.h"
9 #include "DatabaseSync.h"
10 #include "ICalTime.h"
11 #include "RecurrenceExpansion.h"
12
13 #include "ui/ThemeManager.h"
14
15 #include <QFile>
16 #include <QLoggingCategory>
17 #include <QSqlError>
18 #include <QSqlQuery>
19 #include <QTimeZone>
20 #include <QUuid>
21 #include <functional>
22
23
4/7
✓ Branch 2 → 3 taken 54 times.
✓ Branch 2 → 8 taken 9814 times.
✓ Branch 4 → 5 taken 54 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 54 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
9868 Q_LOGGING_CATEGORY(lcCalStore, "mailjd.calendarstore")
24
25 // Write transactions start IMMEDIATE: they take the write lock first and
26 // wait busy_timeout for it. A deferred transaction that has already read
27 // cannot wait — SQLite answers its upgrade with an instant "database is
28 // locked" whenever another connection holds the lock — and the calendar
29 // and task models open their own connections in worker threads. That
30 // failed sync pages and task saves at random (user report 2026-10-13).
31 6931 static bool beginImmediate(QSqlDatabase &db) {
32
1/1
✓ Branch 2 → 3 taken 6931 times.
6931 QSqlQuery q(db);
33
2/3
✓ Branch 6 → 7 taken 6931 times.
✓ Branch 9 → 10 taken 6931 times.
✗ Branch 9 → 11 not taken.
6931 if (q.exec(QStringLiteral("BEGIN IMMEDIATE")))
34 6931 return true;
35 ✗ qCWarning(lcCalStore) << "BEGIN IMMEDIATE:" << q.lastError().text();
36 ✗ return false;
37 6931 }
38
39 static QString serializeReminders(const QList<int> &minutes);
40
41 21311 static bool execMigrationStatement(QSqlQuery &q, const QString &statement,
42 const QString &context) {
43
2/2
✓ Branch 3 → 4 taken 21307 times.
✓ Branch 3 → 5 taken 4 times.
21311 if (q.exec(statement))
44 21307 return true;
45
9/9
✓ Branch 5 → 6 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 20 → 7 taken 4 times.
✓ Branch 20 → 21 taken 4 times.
8 qCWarning(lcCalStore) << context << q.lastError().text() << statement;
46 4 return false;
47 }
48
49 // `backfill` (optional) runs after the statements inside the same
50 // transaction; its failure rolls the step back, so it is retried on the
51 // next open instead of being skipped for good (S106-138).
52 4386 static bool runCalendarMigration(QSqlDatabase &db, QSqlQuery &q,
53 int targetVersion,
54 const QString &context,
55 const QStringList &statements,
56 const std::function<bool()> &backfill = {}) {
57
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 16 taken 4386 times.
4386 if (!beginImmediate(db)) {
58 ✗ qCWarning(lcCalStore) << context << "transaction failed"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; transaction result remains measured.
59 ✗ return false;
60 }
61
62
2/2
✓ Branch 25 → 18 taken 16929 times.
✓ Branch 25 → 26 taken 4382 times.
21311 for (const QString &statement : statements) {
63
3/3
✓ Branch 19 → 20 taken 16929 times.
✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 23 taken 16925 times.
16929 if (!execMigrationStatement(q, statement, context)) {
64
1/1
✓ Branch 21 → 22 taken 4 times.
4 db.rollback();
65 4 return false;
66 }
67 }
68
4/6
✓ Branch 27 → 28 taken 292 times.
✓ Branch 27 → 31 taken 4090 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 292 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 35 taken 4382 times.
4382 if (backfill && !backfill()) {
69 ✗ db.rollback();
70 ✗ return false;
71 }
72
73
1/1
✓ Branch 40 → 41 taken 4382 times.
4382 if (!execMigrationStatement(
74
2/3
✓ Branch 39 → 40 taken 4382 times.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 47 taken 4382 times.
13146 q, QStringLiteral("PRAGMA user_version = %1").arg(targetVersion),
75 context)) {
76 ✗ db.rollback();
77 ✗ return false;
78 }
79
80
1/2
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 66 taken 4382 times.
4382 if (!db.commit()) {
81 ✗ qCWarning(lcCalStore) << context << "commit:" << db.lastError().text(); // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; commit result remains measured.
82 ✗ return false;
83 }
84 4382 return true;
85 }
86
87
1/1
✓ Branch 3 → 4 taken 5051 times.
5051 CalendarStore::CalendarStore(QObject *parent) : QObject(parent) {}
88
89 7640 CalendarStore::~CalendarStore() { close(); }
90
91 318 qint64 CalendarStore::dataVersion() const {
92
1/1
✓ Branch 2 → 3 taken 318 times.
318 QSqlQuery query(m_db);
93
9/20
✓ Branch 6 → 7 taken 318 times.
✓ Branch 7 → 8 taken 318 times.
✗ Branch 7 → 13 not taken.
✓ Branch 8 → 9 taken 318 times.
✓ Branch 9 → 10 taken 318 times.
✗ Branch 9 → 13 not taken.
✓ Branch 10 → 11 taken 318 times.
✓ Branch 11 → 12 taken 318 times.
✓ Branch 15 → 16 taken 318 times.
✗ Branch 15 → 17 not taken.
✓ Branch 17 → 18 taken 318 times.
✗ Branch 17 → 19 not taken.
✓ Branch 19 → 20 taken 318 times.
✗ Branch 19 → 21 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
954 return query.exec(QStringLiteral("PRAGMA data_version")) && query.next() ? query.value(0).toLongLong() : -1;
94 318 }
95
96 5059 bool CalendarStore::open(const QString &dbPath) {
97
3/3
✓ Branch 2 → 3 taken 5059 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 16 taken 5056 times.
5059 if (!DatabaseSecurity::preparePath(dbPath)) {
98
4/4
✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 14 → 6 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
6 qCWarning(lcCalStore)
99
2/2
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
3 << "Failed to create private calendar DB:" << dbPath;
100 3 return false;
101 }
102 m_connectionName =
103 10112 QStringLiteral("calstore_") +
104
4/4
✓ Branch 16 → 17 taken 5056 times.
✓ Branch 17 → 18 taken 5056 times.
✓ Branch 18 → 19 taken 5056 times.
✓ Branch 22 → 23 taken 5056 times.
15168 QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
105
1/1
✓ Branch 32 → 33 taken 5056 times.
10112 m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"),
106
1/1
✓ Branch 33 → 34 taken 5056 times.
10112 m_connectionName);
107
1/1
✓ Branch 37 → 38 taken 5056 times.
5056 m_db.setDatabaseName(dbPath);
108 5056 m_dbPath = dbPath;
109
2/3
✓ Branch 39 → 40 taken 5056 times.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 57 taken 5056 times.
5056 if (!m_db.open()) {
110 ✗ qCWarning(lcCalStore) << "Failed to open DB:" << m_db.lastError().text(); // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; open result remains measured.
111 ✗ return false;
112 }
113
2/3
✓ Branch 57 → 58 taken 5056 times.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 72 taken 5056 times.
5056 if (!DatabaseSecurity::restrictExistingFile(dbPath)) {
114 ✗ qCWarning(lcCalStore) // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; security failure remains measured.
115 ✗ << "Failed to restrict calendar DB permissions:" << dbPath;
116 ✗ m_db.close();
117 ✗ return false;
118 }
119 // WAL mode for concurrent reads
120
1/1
✓ Branch 72 → 73 taken 5056 times.
5056 QSqlQuery pragma(m_db);
121
1/1
✓ Branch 76 → 77 taken 5056 times.
5056 pragma.exec(QStringLiteral("PRAGMA journal_mode=WAL"));
122
1/1
✓ Branch 82 → 83 taken 5056 times.
5056 pragma.exec(QStringLiteral("PRAGMA busy_timeout=5000"));
123
1/1
✓ Branch 88 → 89 taken 5056 times.
5056 pragma.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
124
125
3/3
✓ Branch 91 → 92 taken 5056 times.
✓ Branch 92 → 93 taken 7 times.
✓ Branch 92 → 94 taken 5049 times.
5056 if (!createSchema())
126 7 return false;
127
1/1
✓ Branch 94 → 95 taken 5049 times.
5049 normalizeDefaultPortHrefs();
128 5049 return true;
129 5056 }
130
131 // F93-17: the DAV clients request without the scheme's default port (Qt's
132 // same-origin redirect check compares ports literally). Hrefs stored from
133 // an account URL with an explicit ":443"/":80" are rewritten to the same
134 // form, so href lookups (sync-collection deletions, outbox pushes) keep
135 // matching. Idempotent; touches nothing when no such href exists.
136 5049 void CalendarStore::normalizeDefaultPortHrefs() {
137 15147 const QList<QPair<QString, QString>> columns{{QStringLiteral("events"), QStringLiteral("resource_href")},
138 15147 {QStringLiteral("tasks"), QStringLiteral("resource_href")},
139 15147 {QStringLiteral("calendar_resources"), QStringLiteral("href")},
140
2/4
✓ Branch 32 → 33 taken 20196 times.
✓ Branch 32 → 34 taken 5049 times.
✗ Branch 200 → 201 not taken.
✗ Branch 200 → 202 not taken.
35343 {QStringLiteral("calendar_outbox"), QStringLiteral("href")}};
141
2/2
✓ Branch 196 → 52 taken 20196 times.
✓ Branch 196 → 197 taken 5049 times.
25245 for (const auto &[table, column] : columns) {
142 161568 for (const auto &[scheme, port] : {std::pair{QStringLiteral("https"), QStringLiteral(":443/")},
143
2/2
✓ Branch 187 → 80 taken 40392 times.
✓ Branch 187 → 188 taken 20196 times.
201960 std::pair{QStringLiteral("http"), QStringLiteral(":80/")}}) {
144 // Read first: the write lock only when a row needs it (see above).
145
3/3
✓ Branch 87 → 88 taken 40392 times.
✓ Branch 88 → 89 taken 40392 times.
✓ Branch 89 → 90 taken 40392 times.
80784 const QString pattern = scheme + QStringLiteral("://%") + port + QLatin1Char('%');
146
1/1
✓ Branch 94 → 95 taken 40392 times.
40392 QSqlQuery probe(m_db);
147
2/2
✓ Branch 98 → 99 taken 40392 times.
✓ Branch 99 → 100 taken 40392 times.
40392 probe.prepare(QStringLiteral("SELECT EXISTS (SELECT 1 FROM %1 WHERE %2 LIKE ?)").arg(table, column));
148
1/1
✓ Branch 105 → 106 taken 40392 times.
40392 probe.addBindValue(pattern);
149
11/16
✓ Branch 107 → 108 taken 40392 times.
✓ Branch 108 → 109 taken 40392 times.
✗ Branch 108 → 114 not taken.
✓ Branch 109 → 110 taken 40392 times.
✓ Branch 110 → 111 taken 40392 times.
✗ Branch 110 → 114 not taken.
✓ Branch 111 → 112 taken 40392 times.
✓ Branch 112 → 113 taken 40392 times.
✓ Branch 113 → 114 taken 40391 times.
✓ Branch 113 → 115 taken 1 time.
✓ Branch 116 → 117 taken 40392 times.
✗ Branch 116 → 118 not taken.
✓ Branch 118 → 119 taken 40391 times.
✓ Branch 118 → 120 taken 1 time.
✗ Branch 284 → 285 not taken.
✗ Branch 284 → 286 not taken.
40392 if (!probe.exec() || !probe.next() || !probe.value(0).toBool())
150 40391 continue;
151
1/1
✓ Branch 120 → 121 taken 1 time.
1 probe.finish();
152
1/1
✓ Branch 121 → 122 taken 1 time.
1 QSqlQuery q(m_db);
153
2/2
✓ Branch 125 → 126 taken 1 time.
✓ Branch 126 → 127 taken 1 time.
1 q.prepare(QStringLiteral("UPDATE %1 SET %2 = REPLACE(%2, ?, '/') WHERE %2 LIKE ?").arg(table, column));
154
1/1
✓ Branch 132 → 133 taken 1 time.
1 q.addBindValue(port);
155
1/1
✓ Branch 136 → 137 taken 1 time.
1 q.addBindValue(pattern);
156
2/3
✓ Branch 138 → 139 taken 1 time.
✗ Branch 139 → 140 not taken.
✓ Branch 139 → 158 taken 1 time.
1 if (!q.exec())
157 ✗ qCWarning(lcCalStore) << "Href normalization of" << table << "failed:" << q.lastError().text();
158
2/3
✓ Branch 158 → 159 taken 1 time.
✓ Branch 159 → 160 taken 1 time.
✗ Branch 159 → 175 not taken.
1 else if (q.numRowsAffected() > 0)
159
9/9
✓ Branch 160 → 161 taken 1 time.
✓ Branch 164 → 165 taken 1 time.
✓ Branch 165 → 166 taken 1 time.
✓ Branch 166 → 167 taken 1 time.
✓ Branch 167 → 168 taken 1 time.
✓ Branch 168 → 169 taken 1 time.
✓ Branch 169 → 170 taken 1 time.
✓ Branch 173 → 162 taken 1 time.
✓ Branch 173 → 174 taken 1 time.
2 qCInfo(lcCalStore) << "Removed the default port from" << q.numRowsAffected() << table << "hrefs";
160 141371 }
161 }
162 30294 }
163
164 5177 void CalendarStore::close() {
165
2/2
✓ Branch 3 → 4 taken 5056 times.
✓ Branch 3 → 5 taken 121 times.
5177 if (m_db.isOpen())
166 5056 m_db.close();
167 // T-402/Bug 20: Release reference before removeDatabase
168
2/2
✓ Branch 5 → 6 taken 5177 times.
✓ Branch 6 → 7 taken 5177 times.
5177 m_db = QSqlDatabase();
169
2/2
✓ Branch 9 → 10 taken 5056 times.
✓ Branch 9 → 12 taken 121 times.
5177 if (!m_connectionName.isEmpty()) {
170 5056 QSqlDatabase::removeDatabase(m_connectionName);
171 5056 m_connectionName.clear();
172 }
173 5177 m_dbPath.clear();
174 5177 }
175
176 5034 bool CalendarStore::isOpen() const { return m_db.isOpen(); }
177
178 5056 bool CalendarStore::createSchema() {
179
1/1
✓ Branch 2 → 3 taken 5056 times.
5056 QSqlQuery q(m_db);
180
181
1/1
✓ Branch 6 → 7 taken 5056 times.
5056 bool ok = q.exec(QStringLiteral(
182 "CREATE TABLE IF NOT EXISTS calendars ("
183 " id INTEGER PRIMARY KEY,"
184 " path TEXT UNIQUE NOT NULL,"
185 " displayName TEXT,"
186 " color TEXT,"
187 " ctag TEXT,"
188 " accountId TEXT,"
189 " readOnly INTEGER DEFAULT 0"
190 ")"));
191
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 26 taken 5055 times.
5056 if (!ok) {
192
8/8
✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 24 → 12 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
2 qCWarning(lcCalStore) << "calendars table:" << q.lastError().text();
193 1 return false;
194 }
195
196
1/1
✓ Branch 29 → 30 taken 5055 times.
5055 ok = q.exec(QStringLiteral(
197 "CREATE TABLE IF NOT EXISTS events ("
198 " id INTEGER PRIMARY KEY,"
199 " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
200 " uid TEXT NOT NULL,"
201 " summary TEXT,"
202 " description TEXT,"
203 " location TEXT,"
204 " dt_start INTEGER,"
205 " dt_end INTEGER,"
206 " all_day INTEGER DEFAULT 0,"
207 " rrule TEXT,"
208 " etag TEXT,"
209 " last_modified INTEGER,"
210 " UNIQUE(calendar_id, uid)"
211 ")"));
212
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 49 taken 5054 times.
5055 if (!ok) {
213
8/8
✓ Branch 33 → 34 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 47 → 35 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
2 qCWarning(lcCalStore) << "events table:" << q.lastError().text();
214 1 return false;
215 }
216
217
1/1
✓ Branch 52 → 53 taken 5054 times.
5054 ok = q.exec(QStringLiteral(
218 "CREATE TABLE IF NOT EXISTS tasks ("
219 " id INTEGER PRIMARY KEY,"
220 " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
221 " uid TEXT NOT NULL,"
222 " summary TEXT,"
223 " description TEXT,"
224 " due INTEGER,"
225 " percent_complete INTEGER DEFAULT 0,"
226 " priority INTEGER DEFAULT 0,"
227 " status TEXT DEFAULT 'NEEDS-ACTION',"
228 " completed_at INTEGER,"
229 " etag TEXT,"
230 " last_modified INTEGER,"
231 " related_message_id TEXT,"
232 " UNIQUE(calendar_id, uid)"
233 ")"));
234
2/2
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 72 taken 5053 times.
5054 if (!ok) {
235
8/8
✓ Branch 56 → 57 taken 1 time.
✓ Branch 60 → 61 taken 1 time.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 70 → 58 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
2 qCWarning(lcCalStore) << "tasks table:" << q.lastError().text();
236 1 return false;
237 }
238
239 // Index for date-range queries on events
240
1/1
✓ Branch 75 → 76 taken 5053 times.
5053 q.exec(QStringLiteral(
241 "CREATE INDEX IF NOT EXISTS idx_events_dates ON events(dt_start, dt_end)"));
242
243 // --- Schema migration (Sprint 37 – T-451) ---
244 // Check user_version for migrations
245 5053 int version = 0;
246
7/16
✓ Branch 81 → 82 taken 5053 times.
✓ Branch 82 → 83 taken 5053 times.
✗ Branch 82 → 86 not taken.
✓ Branch 83 → 84 taken 5053 times.
✓ Branch 84 → 85 taken 5053 times.
✗ Branch 84 → 86 not taken.
✓ Branch 87 → 88 taken 5053 times.
✗ Branch 87 → 89 not taken.
✓ Branch 89 → 90 taken 5053 times.
✗ Branch 89 → 91 not taken.
✓ Branch 91 → 92 taken 5053 times.
✗ Branch 91 → 96 not taken.
✗ Branch 1166 → 1167 not taken.
✗ Branch 1166 → 1168 not taken.
✗ Branch 1170 → 1171 not taken.
✗ Branch 1170 → 1172 not taken.
10106 if (q.exec(QStringLiteral("PRAGMA user_version")) && q.next())
247
2/2
✓ Branch 92 → 93 taken 5053 times.
✓ Branch 93 → 94 taken 5053 times.
5053 version = q.value(0).toInt();
248 // Only databases that held data before this start need a backup.
249 5053 const int versionAtOpen = version;
250
251
2/2
✓ Branch 96 → 97 taken 292 times.
✓ Branch 96 → 135 taken 4761 times.
5053 if (version < 1) {
252
5/5
✓ Branch 97 → 98 taken 292 times.
✓ Branch 101 → 102 taken 292 times.
✓ Branch 102 → 103 taken 292 times.
✓ Branch 106 → 99 taken 292 times.
✓ Branch 106 → 107 taken 292 times.
584 qCInfo(lcCalStore) << "Migrating schema v0 → v1 (adding dt_start, created, organizer)";
253
4/6
✓ Branch 126 → 127 taken 876 times.
✓ Branch 126 → 128 taken 292 times.
✓ Branch 132 → 133 taken 1 time.
✓ Branch 132 → 134 taken 291 times.
✗ Branch 1191 → 1192 not taken.
✗ Branch 1191 → 1193 not taken.
1752 if (!runCalendarMigration(
254
1/1
✓ Branch 121 → 122 taken 292 times.
292 m_db, q, 1, QStringLiteral("calendar v1 migration:"),
255 {
256 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN dt_start INTEGER"),
257 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN created INTEGER"),
258 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN organizer TEXT"),
259 })) {
260 1 return false;
261 }
262 291 version = 1;
263 }
264
265
2/2
✓ Branch 135 → 136 taken 292 times.
✓ Branch 135 → 232 taken 4760 times.
5052 if (version < 2) {
266
4/4
✓ Branch 136 → 137 taken 292 times.
✓ Branch 140 → 141 taken 292 times.
✓ Branch 145 → 138 taken 292 times.
✓ Branch 145 → 146 taken 292 times.
584 qCInfo(lcCalStore)
267
1/1
✓ Branch 141 → 142 taken 292 times.
292 << "Migrating schema v1 → v2 (calendar identity includes accountId)";
268
1/1
✓ Branch 149 → 150 taken 292 times.
292 q.exec(QStringLiteral("PRAGMA foreign_keys=OFF"));
269 const QStringList statements = {
270 ✗ QStringLiteral(
271 "CREATE TABLE calendars_new ("
272 " id INTEGER PRIMARY KEY,"
273 " path TEXT NOT NULL,"
274 " displayName TEXT,"
275 " color TEXT,"
276 " ctag TEXT,"
277 " accountId TEXT NOT NULL DEFAULT '',"
278 " readOnly INTEGER DEFAULT 0,"
279 " UNIQUE(accountId, path)"
280 ")"),
281 292 QStringLiteral(
282 "INSERT INTO calendars_new "
283 "(id, path, displayName, color, ctag, accountId, readOnly) "
284 "SELECT id, path, displayName, color, ctag, "
285 "COALESCE(accountId, ''), readOnly FROM calendars"),
286 292 QStringLiteral(
287 "CREATE TABLE events_new ("
288 " id INTEGER PRIMARY KEY,"
289 " calendar_id INTEGER REFERENCES calendars_new(id) ON DELETE CASCADE,"
290 " uid TEXT NOT NULL,"
291 " summary TEXT,"
292 " description TEXT,"
293 " location TEXT,"
294 " dt_start INTEGER,"
295 " dt_end INTEGER,"
296 " all_day INTEGER DEFAULT 0,"
297 " rrule TEXT,"
298 " etag TEXT,"
299 " last_modified INTEGER,"
300 " UNIQUE(calendar_id, uid)"
301 ")"),
302 292 QStringLiteral(
303 "INSERT INTO events_new "
304 "(id, calendar_id, uid, summary, description, location, dt_start, "
305 "dt_end, all_day, rrule, etag, last_modified) "
306 "SELECT id, calendar_id, uid, summary, description, location, "
307 "dt_start, dt_end, all_day, rrule, etag, last_modified FROM events"),
308 292 QStringLiteral(
309 "CREATE TABLE tasks_new ("
310 " id INTEGER PRIMARY KEY,"
311 " calendar_id INTEGER REFERENCES calendars_new(id) ON DELETE CASCADE,"
312 " uid TEXT NOT NULL,"
313 " summary TEXT,"
314 " description TEXT,"
315 " due INTEGER,"
316 " percent_complete INTEGER DEFAULT 0,"
317 " priority INTEGER DEFAULT 0,"
318 " status TEXT DEFAULT 'NEEDS-ACTION',"
319 " completed_at INTEGER,"
320 " etag TEXT,"
321 " last_modified INTEGER,"
322 " dt_start INTEGER,"
323 " created INTEGER,"
324 " organizer TEXT,"
325 " UNIQUE(calendar_id, uid)"
326 ")"),
327 292 QStringLiteral(
328 "INSERT INTO tasks_new "
329 "(id, calendar_id, uid, summary, description, due, "
330 "percent_complete, priority, status, completed_at, etag, "
331 "last_modified, dt_start, created, organizer) "
332 "SELECT id, calendar_id, uid, summary, description, due, "
333 "percent_complete, priority, status, completed_at, etag, "
334 "last_modified, dt_start, created, organizer FROM tasks"),
335 292 QStringLiteral("DROP TABLE tasks"),
336 292 QStringLiteral("DROP TABLE events"),
337 292 QStringLiteral("DROP TABLE calendars"),
338 292 QStringLiteral("ALTER TABLE calendars_new RENAME TO calendars"),
339 292 QStringLiteral("ALTER TABLE events_new RENAME TO events"),
340 292 QStringLiteral("ALTER TABLE tasks_new RENAME TO tasks"),
341
2/4
✓ Branch 190 → 191 taken 3504 times.
✓ Branch 190 → 192 taken 292 times.
✗ Branch 1225 → 1226 not taken.
✗ Branch 1225 → 1227 not taken.
3796 };
342
343
3/3
✓ Branch 208 → 209 taken 292 times.
✓ Branch 212 → 213 taken 1 time.
✓ Branch 212 → 220 taken 291 times.
876 if (!runCalendarMigration(m_db, q, 2,
344 584 QStringLiteral("calendar v2 migration:"),
345 statements)) {
346
1/1
✓ Branch 216 → 217 taken 1 time.
1 q.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
347 1 return false;
348 }
349
1/1
✓ Branch 223 → 224 taken 291 times.
291 q.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
350 291 version = 2;
351 292 }
352
353
2/2
✓ Branch 232 → 233 taken 293 times.
✓ Branch 232 → 267 taken 4758 times.
5051 if (version < 3) {
354
4/4
✓ Branch 233 → 234 taken 293 times.
✓ Branch 237 → 238 taken 293 times.
✓ Branch 242 → 235 taken 293 times.
✓ Branch 242 → 243 taken 293 times.
586 qCInfo(lcCalStore)
355
1/1
✓ Branch 238 → 239 taken 293 times.
293 << "Migrating schema v2 → v3 (storing CalDAV resource hrefs)";
356
4/6
✓ Branch 259 → 260 taken 586 times.
✓ Branch 259 → 261 taken 293 times.
✓ Branch 264 → 265 taken 2 times.
✓ Branch 264 → 266 taken 291 times.
✗ Branch 1308 → 1309 not taken.
✗ Branch 1308 → 1310 not taken.
1465 if (!runCalendarMigration(
357
1/1
✓ Branch 254 → 255 taken 293 times.
293 m_db, q, 3, QStringLiteral("calendar v3 migration:"),
358 {
359 293 QStringLiteral("ALTER TABLE events ADD COLUMN resource_href TEXT"),
360 293 QStringLiteral("ALTER TABLE tasks ADD COLUMN resource_href TEXT"),
361 })) {
362 2 return false;
363 }
364 291 version = 3;
365 }
366
367
2/2
✓ Branch 267 → 268 taken 291 times.
✓ Branch 267 → 298 taken 4758 times.
5049 if (version < 4) {
368
4/4
✓ Branch 268 → 269 taken 291 times.
✓ Branch 272 → 273 taken 291 times.
✓ Branch 277 → 270 taken 291 times.
✓ Branch 277 → 278 taken 291 times.
582 qCInfo(lcCalStore)
369
1/1
✓ Branch 273 → 274 taken 291 times.
291 << "Migrating schema v3 → v4 (EXDATE list, Sprint 79 T-79.C1)";
370
3/6
✓ Branch 291 → 292 taken 291 times.
✓ Branch 291 → 293 taken 291 times.
✗ Branch 295 → 296 not taken.
✓ Branch 295 → 297 taken 291 times.
✗ Branch 1341 → 1342 not taken.
✗ Branch 1341 → 1343 not taken.
1164 if (!runCalendarMigration(
371
1/1
✓ Branch 286 → 287 taken 291 times.
291 m_db, q, 4, QStringLiteral("calendar v4 migration:"),
372 {
373 291 QStringLiteral("ALTER TABLE events ADD COLUMN exdates TEXT"),
374 })) {
375 ✗ return false;
376 }
377 291 version = 4;
378 }
379
380
2/2
✓ Branch 298 → 299 taken 292 times.
✓ Branch 298 → 329 taken 4757 times.
5049 if (version < 5) {
381
4/4
✓ Branch 299 → 300 taken 292 times.
✓ Branch 303 → 304 taken 292 times.
✓ Branch 308 → 301 taken 292 times.
✓ Branch 308 → 309 taken 292 times.
584 qCInfo(lcCalStore)
382
1/1
✓ Branch 304 → 305 taken 292 times.
292 << "Migrating schema v4 → v5 (explicit task due-date semantics)";
383
3/6
✓ Branch 322 → 323 taken 292 times.
✓ Branch 322 → 324 taken 292 times.
✗ Branch 326 → 327 not taken.
✓ Branch 326 → 328 taken 292 times.
✗ Branch 1371 → 1372 not taken.
✗ Branch 1371 → 1373 not taken.
1168 if (!runCalendarMigration(
384
1/1
✓ Branch 317 → 318 taken 292 times.
292 m_db, q, 5, QStringLiteral("calendar v5 migration:"),
385 292 {QStringLiteral(
386 "ALTER TABLE tasks ADD COLUMN due_all_day INTEGER DEFAULT 0")})) {
387 ✗ return false;
388 }
389 292 version = 5;
390 }
391
392
2/2
✓ Branch 329 → 330 taken 292 times.
✓ Branch 329 → 404 taken 4757 times.
5049 if (version < 6) {
393 // Q11: recurrence exceptions share the master's UID; the uniqueness
394 // must include the RECURRENCE-ID ('' for masters). SQLite cannot drop
395 // a UNIQUE constraint, so the table is rebuilt in place. A database
396 // that already carries the column (fixture snapshots that pin an old
397 // user_version on a new schema) skips the ALTER and only rebuilds.
398 QStringList v6Statements = {
399 ✗ QStringLiteral("ALTER TABLE events ADD COLUMN recurrence_id TEXT DEFAULT ''"),
400 292 QStringLiteral("ALTER TABLE events RENAME TO events_old"),
401 292 QStringLiteral(
402 "CREATE TABLE events ("
403 " id INTEGER PRIMARY KEY,"
404 " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
405 " uid TEXT NOT NULL,"
406 " summary TEXT,"
407 " description TEXT,"
408 " location TEXT,"
409 " dt_start INTEGER,"
410 " dt_end INTEGER,"
411 " all_day INTEGER DEFAULT 0,"
412 " rrule TEXT,"
413 " etag TEXT,"
414 " last_modified INTEGER,"
415 " resource_href TEXT,"
416 " exdates TEXT,"
417 " recurrence_id TEXT DEFAULT '',"
418 " UNIQUE(calendar_id, uid, recurrence_id)"
419 ")"),
420 292 QStringLiteral(
421 "INSERT INTO events (id, calendar_id, uid, summary, description, "
422 " location, dt_start, dt_end, all_day, rrule, etag, last_modified, "
423 " resource_href, exdates, recurrence_id) "
424 "SELECT id, calendar_id, uid, summary, description, location, "
425 " dt_start, dt_end, all_day, rrule, etag, last_modified, "
426 " resource_href, exdates, recurrence_id FROM events_old"),
427 292 QStringLiteral("DROP TABLE events_old"),
428
2/4
✓ Branch 347 → 348 taken 1460 times.
✓ Branch 347 → 349 taken 292 times.
✗ Branch 1388 → 1389 not taken.
✗ Branch 1388 → 1390 not taken.
1752 };
429 {
430
1/1
✓ Branch 354 → 355 taken 292 times.
292 QSqlQuery columns(m_db);
431
1/1
✓ Branch 358 → 359 taken 292 times.
292 columns.exec(QStringLiteral("PRAGMA table_info(events)"));
432 292 bool hasRecurrenceId = false;
433
3/3
✓ Branch 373 → 374 taken 4386 times.
✓ Branch 374 → 362 taken 4094 times.
✓ Branch 374 → 375 taken 292 times.
4386 while (columns.next())
434
2/2
✓ Branch 365 → 366 taken 4094 times.
✓ Branch 366 → 367 taken 4094 times.
4094 hasRecurrenceId |= columns.value(1).toString() ==
435 8188 QStringLiteral("recurrence_id");
436
2/2
✓ Branch 375 → 376 taken 1 time.
✓ Branch 375 → 377 taken 291 times.
292 if (hasRecurrenceId)
437 1 v6Statements.removeFirst();
438 292 }
439
4/4
✓ Branch 378 → 379 taken 292 times.
✓ Branch 382 → 383 taken 292 times.
✓ Branch 387 → 380 taken 292 times.
✓ Branch 387 → 388 taken 292 times.
584 qCInfo(lcCalStore)
440
1/1
✓ Branch 383 → 384 taken 292 times.
292 << "Migrating schema v5 → v6 (recurrence exceptions)";
441
1/2
✗ Branch 396 → 397 not taken.
✓ Branch 396 → 398 taken 292 times.
584 if (!runCalendarMigration(
442
1/1
✓ Branch 392 → 393 taken 292 times.
292 m_db, q, 6, QStringLiteral("calendar v6 migration:"),
443 v6Statements)) {
444 ✗ return false;
445 }
446 292 version = 6;
447 292 }
448
449
2/2
✓ Branch 404 → 405 taken 292 times.
✓ Branch 404 → 466 taken 4757 times.
5049 if (version < 7) {
450 292 QStringList v7Statements = {QStringLiteral(
451
2/4
✓ Branch 410 → 411 taken 292 times.
✓ Branch 410 → 412 taken 292 times.
✗ Branch 1449 → 1450 not taken.
✗ Branch 1449 → 1451 not taken.
584 "ALTER TABLE tasks ADD COLUMN related_message_id TEXT")};
452
1/1
✓ Branch 413 → 414 taken 292 times.
292 QSqlQuery columns(m_db);
453
1/1
✓ Branch 417 → 418 taken 292 times.
292 columns.exec(QStringLiteral("PRAGMA table_info(tasks)"));
454
3/3
✓ Branch 434 → 435 taken 5255 times.
✓ Branch 435 → 421 taken 4964 times.
✓ Branch 435 → 436 taken 291 times.
5255 while (columns.next()) {
455
2/2
✓ Branch 424 → 425 taken 4964 times.
✓ Branch 425 → 426 taken 4964 times.
4964 if (columns.value(1).toString() ==
456
2/2
✓ Branch 431 → 432 taken 1 time.
✓ Branch 431 → 434 taken 4963 times.
9928 QStringLiteral("related_message_id")) {
457
1/1
✓ Branch 432 → 433 taken 1 time.
1 v7Statements.clear();
458 1 break;
459 }
460 }
461
4/4
✓ Branch 436 → 437 taken 292 times.
✓ Branch 440 → 441 taken 292 times.
✓ Branch 445 → 438 taken 292 times.
✓ Branch 445 → 446 taken 292 times.
584 qCInfo(lcCalStore)
462
1/1
✓ Branch 441 → 442 taken 292 times.
292 << "Migrating schema v6 → v7 (task mail association)";
463
1/2
✗ Branch 454 → 455 not taken.
✓ Branch 454 → 456 taken 292 times.
584 if (!runCalendarMigration(
464
1/1
✓ Branch 450 → 451 taken 292 times.
292 m_db, q, 7, QStringLiteral("calendar v7 migration:"),
465 v7Statements)) {
466 ✗ return false;
467 }
468 292 version = 7;
469 292 }
470
471
2/2
✓ Branch 466 → 467 taken 292 times.
✓ Branch 466 → 567 taken 4757 times.
5049 if (version < 8) {
472 // Sprint 93 S93.1.1/S93.1.2: lossless round trip and time zones. A
473 // copy of the database is kept until the next successful start so a
474 // failed upgrade can be undone by hand (SPRINT-93 §16).
475
2/2
✓ Branch 467 → 468 taken 1 time.
✓ Branch 467 → 469 taken 291 times.
292 if (versionAtOpen > 0)
476
1/1
✓ Branch 468 → 469 taken 1 time.
1 backupBeforeMigration(8);
477 292 QStringList v8Statements;
478
1/1
✓ Branch 472 → 473 taken 292 times.
292 const QStringList columns = tableColumns(QStringLiteral("events"));
479
1/2
✓ Branch 481 → 482 taken 292 times.
✗ Branch 481 → 489 not taken.
292 if (!columns.contains(QStringLiteral("ical_raw")))
480
1/1
✓ Branch 485 → 486 taken 292 times.
292 v8Statements << QStringLiteral("ALTER TABLE events ADD COLUMN ical_raw TEXT");
481
1/2
✓ Branch 495 → 496 taken 292 times.
✗ Branch 495 → 503 not taken.
292 if (!columns.contains(QStringLiteral("tz_start")))
482
1/1
✓ Branch 499 → 500 taken 292 times.
292 v8Statements << QStringLiteral("ALTER TABLE events ADD COLUMN tz_start TEXT DEFAULT ''");
483
1/2
✓ Branch 509 → 510 taken 292 times.
✗ Branch 509 → 517 not taken.
292 if (!columns.contains(QStringLiteral("tz_end")))
484
1/1
✓ Branch 513 → 514 taken 292 times.
292 v8Statements << QStringLiteral("ALTER TABLE events ADD COLUMN tz_end TEXT DEFAULT ''");
485
1/2
✓ Branch 523 → 524 taken 292 times.
✗ Branch 523 → 531 not taken.
292 if (!columns.contains(QStringLiteral("rdates")))
486
1/1
✓ Branch 527 → 528 taken 292 times.
292 v8Statements << QStringLiteral("ALTER TABLE events ADD COLUMN rdates TEXT");
487
1/1
✓ Branch 534 → 535 taken 292 times.
292 v8Statements << QStringLiteral(
488 "CREATE TABLE IF NOT EXISTS calendar_resources ("
489 " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
490 " uid TEXT NOT NULL,"
491 " href TEXT,"
492 " etag TEXT,"
493 " ical TEXT,"
494 " PRIMARY KEY(calendar_id, uid)"
495 ")");
496
5/5
✓ Branch 537 → 538 taken 292 times.
✓ Branch 541 → 542 taken 292 times.
✓ Branch 542 → 543 taken 292 times.
✓ Branch 546 → 539 taken 292 times.
✓ Branch 546 → 547 taken 292 times.
584 qCInfo(lcCalStore) << "Migrating schema v7 → v8 (raw iCalendar, time zones, RDATE)";
497
2/3
✓ Branch 551 → 552 taken 292 times.
✗ Branch 555 → 556 not taken.
✓ Branch 555 → 557 taken 292 times.
584 if (!runCalendarMigration(m_db, q, 8, QStringLiteral("calendar v8 migration:"),
498 v8Statements)) {
499 ✗ return false;
500 }
501 292 version = 8;
502
2/4
✓ Branch 559 → 560 taken 292 times.
✗ Branch 559 → 561 not taken.
✓ Branch 563 → 564 taken 292 times.
✗ Branch 563 → 566 not taken.
292 } else {
503
1/1
✓ Branch 567 → 568 taken 4757 times.
4757 removeMigrationBackup(8);
504 }
505
506
2/2
✓ Branch 568 → 569 taken 292 times.
✓ Branch 568 → 599 taken 4757 times.
5049 if (version < 9) {
507 // Sprint 93 S93.1.6: persistent write outbox — one row per DAV resource
508 // whose local state still has to reach the server.
509
5/5
✓ Branch 569 → 570 taken 292 times.
✓ Branch 573 → 574 taken 292 times.
✓ Branch 574 → 575 taken 292 times.
✓ Branch 578 → 571 taken 292 times.
✓ Branch 578 → 579 taken 292 times.
584 qCInfo(lcCalStore) << "Migrating schema v8 → v9 (calendar write outbox)";
510
3/6
✓ Branch 592 → 593 taken 292 times.
✓ Branch 592 → 594 taken 292 times.
✗ Branch 596 → 597 not taken.
✓ Branch 596 → 598 taken 292 times.
✗ Branch 1565 → 1566 not taken.
✗ Branch 1565 → 1567 not taken.
1168 if (!runCalendarMigration(
511
1/1
✓ Branch 587 → 588 taken 292 times.
292 m_db, q, 9, QStringLiteral("calendar v9 migration:"),
512 292 {QStringLiteral(
513 "CREATE TABLE IF NOT EXISTS calendar_outbox ("
514 " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
515 " uid TEXT NOT NULL,"
516 " operation TEXT NOT NULL,"
517 " href TEXT,"
518 " base_etag TEXT,"
519 " revision INTEGER DEFAULT 1,"
520 " attempts INTEGER DEFAULT 0,"
521 " next_attempt INTEGER DEFAULT 0,"
522 " last_error TEXT,"
523 " state TEXT DEFAULT 'pending',"
524 " conflict_fields TEXT,"
525 " server_ical TEXT,"
526 " server_etag TEXT,"
527 " created INTEGER,"
528 " PRIMARY KEY(calendar_id, uid)"
529 ")")})) {
530 ✗ return false;
531 }
532 292 version = 9;
533 }
534
535
2/2
✓ Branch 599 → 600 taken 292 times.
✓ Branch 599 → 647 taken 4757 times.
5049 if (version < 10) {
536 // Sprint 93 S93.6.2: incremental sync with RFC 6578 sync-collection.
537 292 QStringList v10Statements;
538
3/3
✓ Branch 603 → 604 taken 292 times.
✓ Branch 613 → 614 taken 291 times.
✓ Branch 613 → 621 taken 1 time.
584 if (!tableColumns(QStringLiteral("calendars")).contains(QStringLiteral("sync_token")))
539
1/1
✓ Branch 617 → 618 taken 291 times.
291 v10Statements << QStringLiteral("ALTER TABLE calendars ADD COLUMN sync_token TEXT DEFAULT ''");
540
5/5
✓ Branch 621 → 622 taken 292 times.
✓ Branch 625 → 626 taken 292 times.
✓ Branch 626 → 627 taken 292 times.
✓ Branch 630 → 623 taken 292 times.
✓ Branch 630 → 631 taken 292 times.
584 qCInfo(lcCalStore) << "Migrating schema v9 → v10 (sync-collection token)";
541
2/3
✓ Branch 635 → 636 taken 292 times.
✗ Branch 639 → 640 not taken.
✓ Branch 639 → 641 taken 292 times.
584 if (!runCalendarMigration(m_db, q, 10, QStringLiteral("calendar v10 migration:"), v10Statements))
542 ✗ return false;
543 292 version = 10;
544 292 }
545
546
2/2
✓ Branch 647 → 648 taken 292 times.
✓ Branch 647 → 701 taken 4757 times.
5049 if (version < 11) {
547 // Sprint 93 S93.5.1: reminders (VALARM) and their local state.
548 292 QStringList v11Statements;
549
2/3
✓ Branch 651 → 652 taken 292 times.
✓ Branch 661 → 662 taken 292 times.
✗ Branch 661 → 669 not taken.
584 if (!tableColumns(QStringLiteral("events")).contains(QStringLiteral("reminders")))
550
1/1
✓ Branch 665 → 666 taken 292 times.
292 v11Statements << QStringLiteral("ALTER TABLE events ADD COLUMN reminders TEXT DEFAULT ''");
551
1/1
✓ Branch 672 → 673 taken 292 times.
292 v11Statements << QStringLiteral(
552 "CREATE TABLE IF NOT EXISTS reminder_state ("
553 " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
554 " uid TEXT NOT NULL,"
555 " occurrence TEXT NOT NULL,"
556 " dismissed INTEGER DEFAULT 0,"
557 " snoozed_until INTEGER DEFAULT 0,"
558 " fired_at INTEGER DEFAULT 0,"
559 " PRIMARY KEY(calendar_id, uid, occurrence)"
560 ")");
561
5/5
✓ Branch 675 → 676 taken 292 times.
✓ Branch 679 → 680 taken 292 times.
✓ Branch 680 → 681 taken 292 times.
✓ Branch 684 → 677 taken 292 times.
✓ Branch 684 → 685 taken 292 times.
584 qCInfo(lcCalStore) << "Migrating schema v10 → v11 (reminders)";
562 // Rows synced since v8 carry their VEVENT: read their alarms once, in
563 // the migration's transaction (S106-138: a failure used to be ignored
564 // while the version advanced, so those alarms never fired).
565 292 const auto backfillReminders = [this] {
566
1/1
✓ Branch 2 → 3 taken 292 times.
292 QSqlQuery rows(m_db);
567
2/3
✓ Branch 6 → 7 taken 292 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 26 taken 292 times.
292 if (!rows.exec(QStringLiteral("SELECT id, ical_raw FROM events WHERE ical_raw IS NOT NULL AND ical_raw != ''"))) {
568 ✗ qCWarning(lcCalStore) << "calendar v11 reminder backfill:" << rows.lastError().text();
569 ✗ return false;
570 }
571
1/1
✓ Branch 26 → 27 taken 292 times.
292 QSqlQuery update(m_db);
572
1/1
✓ Branch 30 → 31 taken 292 times.
292 update.prepare(QStringLiteral("UPDATE events SET reminders = ? WHERE id = ?"));
573
2/3
✓ Branch 77 → 78 taken 292 times.
✗ Branch 78 → 34 not taken.
✓ Branch 78 → 79 taken 292 times.
292 while (rows.next()) {
574 ✗ const QList<int> minutes = ICalEventCodec::reminderMinutes(rows.value(1).toString());
575 ✗ if (minutes.isEmpty()) continue;
576 ✗ update.addBindValue(serializeReminders(minutes));
577 ✗ update.addBindValue(rows.value(0));
578 ✗ if (!update.exec()) {
579 ✗ qCWarning(lcCalStore) << "calendar v11 reminder backfill:" << update.lastError().text();
580 ✗ return false;
581 }
582 ✗ }
583 292 return true;
584 292 };
585
2/3
✓ Branch 689 → 690 taken 292 times.
✗ Branch 693 → 694 not taken.
✓ Branch 693 → 695 taken 292 times.
584 if (!runCalendarMigration(m_db, q, 11, QStringLiteral("calendar v11 migration:"), v11Statements,
586 backfillReminders))
587 ✗ return false;
588 292 version = 11;
589 292 }
590
591
1/1
✓ Branch 704 → 705 taken 5049 times.
5049 q.exec(QStringLiteral(
592 "CREATE INDEX IF NOT EXISTS idx_events_dates ON events(dt_start, dt_end)"));
593 // CAL-PERF-YEAR: eventsForDateRange() reads single events by their end and
594 // recurring masters through this partial index (same expression as the
595 // query, otherwise SQLite does not use it) instead of scanning every row.
596
1/1
✓ Branch 710 → 711 taken 5049 times.
5049 q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_events_end ON events(dt_end)"));
597
1/1
✓ Branch 716 → 717 taken 5049 times.
5049 q.exec(QStringLiteral("CREATE INDEX IF NOT EXISTS idx_events_recurring ON events(dt_start) "
598 "WHERE COALESCE(rrule, '') != '' OR COALESCE(rdates, '') != ''"));
599
600
2/2
✓ Branch 719 → 720 taken 292 times.
✓ Branch 719 → 802 taken 4757 times.
5049 if (version < 12) {
601
3/3
✓ Branch 720 → 721 taken 1 time.
✓ Branch 720 → 722 taken 291 times.
✓ Branch 721 → 722 taken 1 time.
292 if (versionAtOpen > 0) backupBeforeMigration(12);
602
4/7
✓ Branch 775 → 776 taken 292 times.
✓ Branch 780 → 781 taken 4672 times.
✓ Branch 780 → 782 taken 292 times.
✗ Branch 799 → 800 not taken.
✓ Branch 799 → 801 taken 292 times.
✗ Branch 1674 → 1675 not taken.
✗ Branch 1674 → 1676 not taken.
5548 if (!runCalendarMigration(m_db, q, 12, QStringLiteral("calendar v12 migration:"), {
603 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN ical_raw TEXT"),
604 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN tz_due TEXT DEFAULT ''"),
605 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN tz_start TEXT DEFAULT ''"),
606 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN start_all_day INTEGER DEFAULT 0"),
607 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN parent_uid TEXT DEFAULT ''"),
608 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN rrule TEXT DEFAULT ''"),
609 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT ''"),
610 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN alarms TEXT DEFAULT ''"),
611 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN sort_order INTEGER DEFAULT 0"),
612 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN previous_status TEXT DEFAULT ''"),
613 292 QStringLiteral("ALTER TABLE tasks ADD COLUMN previous_percent INTEGER DEFAULT 0"),
614 292 QStringLiteral("ALTER TABLE calendars ADD COLUMN supported_components TEXT DEFAULT ''"),
615 292 QStringLiteral("ALTER TABLE calendar_outbox ADD COLUMN component_type TEXT DEFAULT 'VEVENT'"),
616 292 QStringLiteral("ALTER TABLE calendar_outbox ADD COLUMN dependency_key TEXT DEFAULT ''"),
617 292 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(calendar_id, parent_uid)"),
618 292 QStringLiteral("CREATE INDEX IF NOT EXISTS idx_tasks_mail ON tasks(related_message_id)")})) return false;
619 292 version = 12;
620
1/1
✓ Branch 802 → 803 taken 4757 times.
4757 } else { removeMigrationBackup(12); }
621
622 // CAL-PERF-YEAR: the end of a series' last occurrence (NULL = unbounded),
623 // so period loads skip series that ended before the period.
624
2/2
✓ Branch 803 → 804 taken 293 times.
✓ Branch 803 → 874 taken 4756 times.
5049 if (version < 13) {
625
3/3
✓ Branch 804 → 805 taken 2 times.
✓ Branch 804 → 806 taken 291 times.
✓ Branch 805 → 806 taken 2 times.
293 if (versionAtOpen > 0) backupBeforeMigration(13);
626
2/3
✓ Branch 806 → 807 taken 293 times.
✗ Branch 807 → 808 not taken.
✓ Branch 807 → 809 taken 293 times.
293 if (!beginImmediate(m_db)) return false;
627
1/1
✓ Branch 812 → 813 taken 293 times.
586 const bool hasColumn = tableColumns(QStringLiteral("events")).contains(QStringLiteral("series_end"));
628
8/18
✓ Branch 822 → 823 taken 293 times.
✗ Branch 822 → 828 not taken.
✓ Branch 826 → 827 taken 293 times.
✓ Branch 827 → 828 taken 293 times.
✗ Branch 827 → 830 not taken.
✓ Branch 828 → 829 taken 293 times.
✗ Branch 829 → 830 not taken.
✓ Branch 829 → 831 taken 293 times.
✓ Branch 832 → 833 taken 293 times.
✗ Branch 832 → 834 not taken.
✓ Branch 834 → 835 taken 293 times.
✗ Branch 834 → 836 not taken.
✗ Branch 836 → 837 not taken.
✓ Branch 836 → 854 taken 293 times.
✗ Branch 1742 → 1743 not taken.
✗ Branch 1742 → 1744 not taken.
✗ Branch 1746 → 1747 not taken.
✗ Branch 1746 → 1748 not taken.
586 if ((!hasColumn && !q.exec(QStringLiteral("ALTER TABLE events ADD COLUMN series_end INTEGER"))) || !backfillSeriesEnds()) {
629 ✗ qCWarning(lcCalStore) << "calendar v13 migration:" << q.lastError().text();
630 ✗ m_db.rollback();
631 ✗ return false;
632 }
633
1/1
✓ Branch 857 → 858 taken 293 times.
293 q.exec(QStringLiteral("PRAGMA user_version = 13"));
634
2/3
✓ Branch 860 → 861 taken 293 times.
✗ Branch 861 → 862 not taken.
✓ Branch 861 → 863 taken 293 times.
293 if (!m_db.commit()) return false;
635
5/5
✓ Branch 863 → 864 taken 293 times.
✓ Branch 867 → 868 taken 293 times.
✓ Branch 868 → 869 taken 293 times.
✓ Branch 872 → 865 taken 293 times.
✓ Branch 872 → 873 taken 293 times.
586 qCInfo(lcCalStore) << "Migrating schema v12 → v13 (series end)";
636 293 version = 13;
637
1/1
✓ Branch 874 → 875 taken 4756 times.
4756 } else { removeMigrationBackup(13); }
638
639 // Sprint 96 S96.3.0 (CAL-SYNC-TRUNCATION): a token-less sync-collection
640 // that the server answers in several pages (RFC 6578 507) is an
641 // enumeration: its checkpoint token and the UIDs seen so far persist, so
642 // pruning happens only after the last page, also across restarts.
643
2/2
✓ Branch 875 → 876 taken 293 times.
✓ Branch 875 → 948 taken 4756 times.
5049 if (version < 14) {
644
3/3
✓ Branch 876 → 877 taken 2 times.
✓ Branch 876 → 878 taken 291 times.
✓ Branch 877 → 878 taken 2 times.
293 if (versionAtOpen > 0) backupBeforeMigration(14);
645 293 QStringList statements;
646
1/1
✓ Branch 881 → 882 taken 293 times.
293 const QStringList calendarColumns = tableColumns(QStringLiteral("calendars"));
647
2/2
✓ Branch 890 → 891 taken 291 times.
✓ Branch 890 → 898 taken 2 times.
293 if (!calendarColumns.contains(QStringLiteral("enum_active")))
648
1/1
✓ Branch 894 → 895 taken 291 times.
291 statements << QStringLiteral("ALTER TABLE calendars ADD COLUMN enum_active INTEGER DEFAULT 0");
649
2/2
✓ Branch 904 → 905 taken 291 times.
✓ Branch 904 → 912 taken 2 times.
293 if (!calendarColumns.contains(QStringLiteral("enum_token")))
650
1/1
✓ Branch 908 → 909 taken 291 times.
291 statements << QStringLiteral("ALTER TABLE calendars ADD COLUMN enum_token TEXT DEFAULT ''");
651
1/1
✓ Branch 915 → 916 taken 293 times.
293 statements << QStringLiteral(
652 "CREATE TABLE IF NOT EXISTS calendar_enum_seen ("
653 " calendar_id INTEGER NOT NULL REFERENCES calendars(id) ON DELETE CASCADE,"
654 " uid TEXT NOT NULL,"
655 " PRIMARY KEY(calendar_id, uid))");
656
2/3
✓ Branch 922 → 923 taken 293 times.
✗ Branch 926 → 927 not taken.
✓ Branch 926 → 928 taken 293 times.
586 if (!runCalendarMigration(m_db, q, 14, QStringLiteral("calendar v14 migration:"), statements))
657 ✗ return false;
658
5/5
✓ Branch 928 → 929 taken 293 times.
✓ Branch 932 → 933 taken 293 times.
✓ Branch 933 → 934 taken 293 times.
✓ Branch 937 → 930 taken 293 times.
✓ Branch 937 → 938 taken 293 times.
586 qCInfo(lcCalStore) << "Migrating schema v13 → v14 (sync enumeration checkpoint)";
659 293 version = 14;
660
3/5
✓ Branch 940 → 941 taken 293 times.
✗ Branch 940 → 942 not taken.
✓ Branch 944 → 945 taken 293 times.
✗ Branch 944 → 947 not taken.
✓ Branch 948 → 949 taken 4756 times.
5049 } else { removeMigrationBackup(14); }
661
662 // Sprint 103 S103.1.10: people named in events (CONTACT) and attendees,
663 // indexed for the contact history (CalendarPeopleIndex).
664
2/2
✓ Branch 949 → 950 taken 294 times.
✓ Branch 949 → 992 taken 4755 times.
5049 if (version < 15) {
665
3/3
✓ Branch 950 → 951 taken 3 times.
✓ Branch 950 → 952 taken 291 times.
✓ Branch 951 → 952 taken 3 times.
294 if (versionAtOpen > 0) backupBeforeMigration(15);
666
2/3
✓ Branch 957 → 958 taken 294 times.
✗ Branch 962 → 963 not taken.
✓ Branch 962 → 964 taken 294 times.
882 if (!runCalendarMigration(m_db, q, 15, QStringLiteral("calendar v15 migration:"),
667
1/1
✓ Branch 953 → 954 taken 294 times.
588 CalendarPeopleIndex::schemaStatements()))
668 ✗ return false;
669
5/7
✓ Branch 964 → 965 taken 3 times.
✓ Branch 964 → 968 taken 291 times.
✓ Branch 965 → 966 taken 3 times.
✗ Branch 966 → 967 not taken.
✓ Branch 966 → 968 taken 3 times.
✗ Branch 969 → 970 not taken.
✓ Branch 969 → 981 taken 294 times.
294 if (versionAtOpen > 0 && !CalendarPeopleIndex::backfill(m_db))
670 ✗ qCWarning(lcCalStore) << "calendar v15: people index incomplete";
671
5/5
✓ Branch 981 → 982 taken 294 times.
✓ Branch 985 → 986 taken 294 times.
✓ Branch 986 → 987 taken 294 times.
✓ Branch 990 → 983 taken 294 times.
✓ Branch 990 → 991 taken 294 times.
588 qCInfo(lcCalStore) << "Migrating schema v14 → v15 (people in events)";
672 294 version = 15;
673
1/1
✓ Branch 992 → 993 taken 4755 times.
4755 } else { removeMigrationBackup(15); }
674
675 // Sprint 107 D-21: the losing side of every conflict resolution.
676
2/2
✓ Branch 993 → 994 taken 295 times.
✓ Branch 993 → 1021 taken 4754 times.
5049 if (version < 16) {
677
4/7
✓ Branch 1000 → 1001 taken 295 times.
✓ Branch 1005 → 1006 taken 295 times.
✓ Branch 1005 → 1007 taken 295 times.
✗ Branch 1008 → 1009 not taken.
✓ Branch 1008 → 1010 taken 295 times.
✗ Branch 1846 → 1847 not taken.
✗ Branch 1846 → 1848 not taken.
885 if (!runCalendarMigration(m_db, q, 16, QStringLiteral("calendar v16 migration:"),
678 {ConflictHistory::createStatement()}))
679 ✗ return false;
680
5/5
✓ Branch 1010 → 1011 taken 295 times.
✓ Branch 1014 → 1015 taken 295 times.
✓ Branch 1015 → 1016 taken 295 times.
✓ Branch 1019 → 1012 taken 295 times.
✓ Branch 1019 → 1020 taken 295 times.
590 qCInfo(lcCalStore) << "Migrating schema v15 → v16 (conflict history)";
681 295 version = 16;
682 }
683
684 // S106-125: an UNTIL that is no real date stored a series end before the
685 // series' start (1970), which hid the whole series from every range query.
686 // No valid row has such an end; clearing it (= unbounded) on every open
687 // repairs rows written before the fix. Idempotent and cheap.
688 // Read first: an open takes the write lock only when there is something
689 // to repair (every model reload opens a connection).
690 5049 const QString impossibleEnd = QStringLiteral(
691 "series_end IS NOT NULL AND dt_start IS NOT NULL AND series_end < dt_start");
692
9/19
✓ Branch 1029 → 1030 taken 5049 times.
✓ Branch 1030 → 1031 taken 5049 times.
✓ Branch 1031 → 1032 taken 5049 times.
✗ Branch 1031 → 1038 not taken.
✓ Branch 1032 → 1033 taken 5049 times.
✓ Branch 1033 → 1034 taken 5049 times.
✗ Branch 1033 → 1038 not taken.
✓ Branch 1043 → 1044 taken 5049 times.
✗ Branch 1043 → 1045 not taken.
✓ Branch 1045 → 1046 taken 5049 times.
✗ Branch 1045 → 1047 not taken.
✓ Branch 1047 → 1048 taken 1 time.
✓ Branch 1047 → 1091 taken 5048 times.
✗ Branch 1868 → 1869 not taken.
✗ Branch 1868 → 1870 not taken.
✗ Branch 1873 → 1874 not taken.
✗ Branch 1873 → 1875 not taken.
✗ Branch 1877 → 1878 not taken.
✗ Branch 1877 → 1879 not taken.
20196 if (q.exec(QStringLiteral("SELECT EXISTS (SELECT 1 FROM events WHERE %1)").arg(impossibleEnd)) && q.next() &&
693
6/10
✓ Branch 1034 → 1035 taken 5049 times.
✓ Branch 1035 → 1036 taken 5049 times.
✓ Branch 1036 → 1037 taken 1 time.
✓ Branch 1036 → 1038 taken 5048 times.
✓ Branch 1039 → 1040 taken 5049 times.
✗ Branch 1039 → 1041 not taken.
✓ Branch 1041 → 1042 taken 5049 times.
✗ Branch 1041 → 1043 not taken.
✗ Branch 1864 → 1865 not taken.
✗ Branch 1864 → 1866 not taken.
10098 q.value(0).toBool()) {
694
1/1
✓ Branch 1048 → 1049 taken 1 time.
1 q.finish();
695
3/4
✓ Branch 1053 → 1054 taken 1 time.
✓ Branch 1054 → 1055 taken 1 time.
✗ Branch 1058 → 1059 not taken.
✓ Branch 1058 → 1075 taken 1 time.
2 if (!q.exec(QStringLiteral("UPDATE events SET series_end = NULL WHERE %1").arg(impossibleEnd)))
696 ✗ qCWarning(lcCalStore) << "series_end repair failed:" << q.lastError().text();
697
2/3
✓ Branch 1075 → 1076 taken 1 time.
✓ Branch 1076 → 1077 taken 1 time.
✗ Branch 1076 → 1091 not taken.
1 else if (q.numRowsAffected() > 0)
698
8/8
✓ Branch 1077 → 1078 taken 1 time.
✓ Branch 1081 → 1082 taken 1 time.
✓ Branch 1082 → 1083 taken 1 time.
✓ Branch 1083 → 1084 taken 1 time.
✓ Branch 1084 → 1085 taken 1 time.
✓ Branch 1085 → 1086 taken 1 time.
✓ Branch 1089 → 1079 taken 1 time.
✓ Branch 1089 → 1090 taken 1 time.
2 qCInfo(lcCalStore) << "cleared" << q.numRowsAffected() << "impossible series ends (S106-125)";
699 }
700
1/1
✓ Branch 1091 → 1092 taken 5049 times.
5049 q.finish();
701
702
703
5/5
✓ Branch 1092 → 1093 taken 5049 times.
✓ Branch 1096 → 1097 taken 5049 times.
✓ Branch 1097 → 1098 taken 5049 times.
✓ Branch 1104 → 1094 taken 5049 times.
✓ Branch 1104 → 1105 taken 5049 times.
10098 qCDebug(lcCalStore) << "Schema created/verified (version:"
704
2/2
✓ Branch 1099 → 1100 taken 5049 times.
✓ Branch 1100 → 1101 taken 5049 times.
5049 << qMax(version, SchemaVersion) << ")";
705 5049 return true;
706 20244 }
707
708 1462 QStringList CalendarStore::tableColumns(const QString &table) const {
709 1462 QStringList columns;
710
1/1
✓ Branch 2 → 3 taken 1462 times.
1462 QSqlQuery query(m_db);
711
2/2
✓ Branch 7 → 8 taken 1462 times.
✓ Branch 8 → 9 taken 1462 times.
2924 query.exec(QStringLiteral("PRAGMA table_info(%1)").arg(table));
712
3/3
✓ Branch 19 → 20 taken 21938 times.
✓ Branch 20 → 13 taken 20476 times.
✓ Branch 20 → 21 taken 1462 times.
21938 while (query.next())
713
3/3
✓ Branch 13 → 14 taken 20476 times.
✓ Branch 14 → 15 taken 20476 times.
✓ Branch 15 → 16 taken 20476 times.
20476 columns.append(query.value(1).toString());
714 1462 return columns;
715 1462 }
716
717 23790 QString CalendarStore::migrationBackupPath(int version) const {
718
2/2
✓ Branch 6 → 7 taken 23790 times.
✓ Branch 7 → 8 taken 23790 times.
47580 return m_dbPath + QStringLiteral(".pre-v%1.bak").arg(version);
719 }
720
721 9 void CalendarStore::backupBeforeMigration(int version) {
722
3/6
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 9 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 9 times.
9 if (m_dbPath.isEmpty() || m_dbPath == QLatin1String(":memory:"))
723 ✗ return;
724
1/1
✓ Branch 11 → 12 taken 9 times.
9 QSqlQuery checkpoint(m_db);
725
1/1
✓ Branch 15 → 16 taken 9 times.
9 checkpoint.exec(QStringLiteral("PRAGMA wal_checkpoint(TRUNCATE)"));
726
1/1
✓ Branch 18 → 19 taken 9 times.
9 const QString backup = migrationBackupPath(version);
727
1/1
✓ Branch 19 → 20 taken 9 times.
9 QFile::remove(backup);
728
2/3
✓ Branch 20 → 21 taken 9 times.
✓ Branch 21 → 22 taken 9 times.
✗ Branch 21 → 35 not taken.
9 if (QFile::copy(m_dbPath, backup)) {
729
1/1
✓ Branch 22 → 23 taken 9 times.
9 DatabaseSecurity::restrictExistingFile(backup);
730
6/6
✓ Branch 23 → 24 taken 9 times.
✓ Branch 27 → 28 taken 9 times.
✓ Branch 28 → 29 taken 9 times.
✓ Branch 29 → 30 taken 9 times.
✓ Branch 33 → 25 taken 9 times.
✓ Branch 33 → 34 taken 9 times.
18 qCInfo(lcCalStore) << "Pre-migration backup written:" << backup;
731 } else {
732 ✗ qCWarning(lcCalStore) << "Pre-migration backup failed:" << backup;
733 }
734 9 }
735
736 23781 void CalendarStore::removeMigrationBackup(int version) {
737
1/1
✓ Branch 2 → 3 taken 23781 times.
23781 const QString backup = migrationBackupPath(version);
738
4/10
✓ Branch 4 → 5 taken 23781 times.
✗ Branch 4 → 10 not taken.
✓ Branch 5 → 6 taken 23781 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 10 taken 23781 times.
✗ Branch 7 → 8 not taken.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 24 taken 23781 times.
23781 if (!m_dbPath.isEmpty() && QFile::exists(backup) && QFile::remove(backup))
739 ✗ qCInfo(lcCalStore) << "Removed pre-migration backup after a successful start:" << backup;
740 23781 }
741
742 152 qint64 CalendarStore::calendarIdForPath(const QString &path) const {
743
1/1
✓ Branch 2 → 3 taken 152 times.
152 QSqlQuery q(m_db);
744
1/1
✓ Branch 6 → 7 taken 152 times.
152 q.prepare(QStringLiteral("SELECT id FROM calendars WHERE path = ? "
745 "ORDER BY id LIMIT 2"));
746
1/1
✓ Branch 11 → 12 taken 152 times.
152 q.addBindValue(path);
747
8/8
✓ Branch 13 → 14 taken 152 times.
✓ Branch 14 → 15 taken 142 times.
✓ Branch 14 → 17 taken 10 times.
✓ Branch 15 → 16 taken 142 times.
✓ Branch 16 → 17 taken 8 times.
✓ Branch 16 → 18 taken 134 times.
✓ Branch 19 → 20 taken 18 times.
✓ Branch 19 → 21 taken 134 times.
152 if (!q.exec() || !q.next())
748 18 return -1;
749
2/2
✓ Branch 21 → 22 taken 134 times.
✓ Branch 22 → 23 taken 134 times.
134 const qint64 id = q.value(0).toLongLong();
750
3/3
✓ Branch 24 → 25 taken 134 times.
✓ Branch 25 → 26 taken 5 times.
✓ Branch 25 → 38 taken 129 times.
134 if (q.next()) {
751
4/4
✓ Branch 26 → 27 taken 5 times.
✓ Branch 30 → 31 taken 5 times.
✓ Branch 36 → 28 taken 5 times.
✓ Branch 36 → 37 taken 5 times.
10 qCWarning(lcCalStore)
752
2/2
✓ Branch 31 → 32 taken 5 times.
✓ Branch 32 → 33 taken 5 times.
5 << "calendarIdForPath: ambiguous path without accountId:" << path;
753 5 return -1;
754 }
755 129 return id;
756 152 }
757
758 70386 qint64 CalendarStore::calendarIdForPath(const QString &accountId,
759 const QString &path) const {
760
2/2
✓ Branch 3 → 4 taken 71 times.
✓ Branch 3 → 6 taken 70315 times.
70386 if (accountId.isEmpty())
761
1/1
✓ Branch 4 → 5 taken 71 times.
71 return calendarIdForPath(path);
762
763
1/1
✓ Branch 6 → 7 taken 70315 times.
70315 QSqlQuery q(m_db);
764
1/1
✓ Branch 10 → 11 taken 70315 times.
70315 q.prepare(QStringLiteral(
765 "SELECT id FROM calendars WHERE accountId = ? AND path = ?"));
766
1/1
✓ Branch 15 → 16 taken 70315 times.
70315 q.addBindValue(accountId);
767
1/1
✓ Branch 19 → 20 taken 70315 times.
70315 q.addBindValue(path);
768
8/8
✓ Branch 21 → 22 taken 70315 times.
✓ Branch 22 → 23 taken 70309 times.
✓ Branch 22 → 26 taken 6 times.
✓ Branch 23 → 24 taken 70309 times.
✓ Branch 24 → 25 taken 70292 times.
✓ Branch 24 → 26 taken 17 times.
✓ Branch 27 → 28 taken 70292 times.
✓ Branch 27 → 33 taken 23 times.
70315 if (q.exec() && q.next())
769
2/2
✓ Branch 28 → 29 taken 70292 times.
✓ Branch 29 → 30 taken 70292 times.
70292 return q.value(0).toLongLong();
770 23 return -1;
771 70315 }
772
773 // ═══════════════════════════════════════════════════════
774 // Calendars
775 // ═══════════════════════════════════════════════════════
776
777 911 void CalendarStore::upsertCalendar(const CalendarInfo &cal,
778 const QString &accountId) {
779
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 910 times.
911 const QString owner = accountId.isEmpty() ? cal.accountId : accountId;
780 // Determine color: use server color, keep existing custom color, or generate
781 // a deterministic palette color for new calendars without server color
782 911 QString color = cal.color;
783
2/2
✓ Branch 9 → 10 taken 497 times.
✓ Branch 9 → 27 taken 414 times.
911 if (color.isEmpty()) {
784
1/1
✓ Branch 10 → 11 taken 497 times.
497 QString existing = calendarColor(owner, cal.path);
785
2/2
✓ Branch 12 → 13 taken 440 times.
✓ Branch 12 → 25 taken 57 times.
497 if (existing.isEmpty()) {
786 // Shared palette (67.B3: ThemeManager owns all color decisions)
787
1/1
✓ Branch 13 → 14 taken 440 times.
440 const QStringList palette = ThemeManager::calendarPalette();
788 440 color = palette.at(
789
2/2
✓ Branch 16 → 17 taken 440 times.
✓ Branch 17 → 18 taken 440 times.
440 qHash(owner + QLatin1Char('\0') + cal.path) % palette.size());
790 440 }
791 497 }
792
793
1/1
✓ Branch 27 → 28 taken 911 times.
911 QSqlQuery q(m_db);
794
1/1
✓ Branch 31 → 32 taken 911 times.
911 q.prepare(QStringLiteral(
795 "INSERT INTO calendars (path, displayName, color, ctag, accountId, readOnly, supported_components) "
796 "VALUES (?, ?, ?, ?, ?, ?, ?) "
797 "ON CONFLICT(accountId, path) DO UPDATE SET "
798 " displayName = excluded.displayName,"
799 " ctag = excluded.ctag,"
800 " readOnly = excluded.readOnly, supported_components = excluded.supported_components"));
801
1/1
✓ Branch 36 → 37 taken 911 times.
911 q.addBindValue(cal.path);
802
1/1
✓ Branch 40 → 41 taken 911 times.
911 q.addBindValue(cal.displayName);
803
1/1
✓ Branch 44 → 45 taken 911 times.
911 q.addBindValue(color);
804
1/1
✓ Branch 48 → 49 taken 911 times.
911 q.addBindValue(cal.ctag);
805
1/1
✓ Branch 52 → 53 taken 911 times.
911 q.addBindValue(owner);
806
3/3
✓ Branch 55 → 56 taken 36 times.
✓ Branch 55 → 57 taken 875 times.
✓ Branch 59 → 60 taken 911 times.
911 q.addBindValue(cal.readOnly ? 1 : 0);
807
2/2
✓ Branch 63 → 64 taken 911 times.
✓ Branch 65 → 66 taken 911 times.
911 q.addBindValue(cal.supportedComponents.join(','));
808
3/3
✓ Branch 68 → 69 taken 911 times.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 86 taken 910 times.
911 if (!q.exec())
809
8/8
✓ Branch 70 → 71 taken 1 time.
✓ Branch 74 → 75 taken 1 time.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 84 → 72 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
2 qCWarning(lcCalStore) << "upsertCalendar:" << q.lastError().text();
810 911 }
811
812 160 QString CalendarStore::calendarCtag(const QString &accountId, const QString &path) const {
813
1/1
✓ Branch 2 → 3 taken 160 times.
160 QSqlQuery q(m_db);
814
1/1
✓ Branch 6 → 7 taken 160 times.
160 q.prepare(QStringLiteral("SELECT ctag FROM calendars WHERE accountId = ? AND path = ?"));
815
1/1
✓ Branch 11 → 12 taken 160 times.
160 q.addBindValue(accountId);
816
1/1
✓ Branch 15 → 16 taken 160 times.
160 q.addBindValue(path);
817
7/8
✓ Branch 17 → 18 taken 160 times.
✓ Branch 18 → 19 taken 160 times.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 160 times.
✓ Branch 20 → 21 taken 101 times.
✓ Branch 20 → 22 taken 59 times.
✓ Branch 23 → 24 taken 101 times.
✓ Branch 23 → 25 taken 59 times.
160 if (!q.exec() || !q.next())
818 101 return {};
819
2/2
✓ Branch 25 → 26 taken 59 times.
✓ Branch 26 → 27 taken 59 times.
59 return q.value(0).toString();
820 160 }
821
822 100 bool CalendarStore::setCalendarCtag(const QString &accountId, const QString &path,
823 const QString &ctag) {
824
1/1
✓ Branch 2 → 3 taken 100 times.
100 QSqlQuery q(m_db);
825
1/1
✓ Branch 6 → 7 taken 100 times.
100 q.prepare(QStringLiteral("UPDATE calendars SET ctag = ? WHERE accountId = ? AND path = ?"));
826
1/1
✓ Branch 11 → 12 taken 100 times.
100 q.addBindValue(ctag);
827
1/1
✓ Branch 15 → 16 taken 100 times.
100 q.addBindValue(accountId);
828
1/1
✓ Branch 19 → 20 taken 100 times.
100 q.addBindValue(path);
829
2/3
✓ Branch 21 → 22 taken 100 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 39 taken 100 times.
100 if (!q.exec()) {
830 ✗ qCWarning(lcCalStore) << "setCalendarCtag:" << q.lastError().text();
831 ✗ return false;
832 }
833
1/1
✓ Branch 39 → 40 taken 100 times.
100 return q.numRowsAffected() == 1;
834 100 }
835
836 186 QString CalendarStore::calendarSyncToken(const QString &accountId, const QString &path) const {
837
1/1
✓ Branch 2 → 3 taken 186 times.
186 QSqlQuery q(m_db);
838
1/1
✓ Branch 6 → 7 taken 186 times.
186 q.prepare(QStringLiteral("SELECT sync_token FROM calendars WHERE accountId = ? AND path = ?"));
839
1/1
✓ Branch 11 → 12 taken 186 times.
186 q.addBindValue(accountId);
840
1/1
✓ Branch 15 → 16 taken 186 times.
186 q.addBindValue(path);
841
5/8
✓ Branch 17 → 18 taken 186 times.
✓ Branch 18 → 19 taken 186 times.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 186 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 186 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 186 times.
186 if (!q.exec() || !q.next())
842 ✗ return {};
843
2/2
✓ Branch 25 → 26 taken 186 times.
✓ Branch 26 → 27 taken 186 times.
186 return q.value(0).toString();
844 186 }
845
846 165 bool CalendarStore::setCalendarSyncToken(const QString &accountId, const QString &path,
847 const QString &token) {
848
1/1
✓ Branch 2 → 3 taken 165 times.
165 QSqlQuery q(m_db);
849
1/1
✓ Branch 6 → 7 taken 165 times.
165 q.prepare(QStringLiteral("UPDATE calendars SET sync_token = ? WHERE accountId = ? AND path = ?"));
850
1/1
✓ Branch 11 → 12 taken 165 times.
165 q.addBindValue(token);
851
1/1
✓ Branch 15 → 16 taken 165 times.
165 q.addBindValue(accountId);
852
1/1
✓ Branch 19 → 20 taken 165 times.
165 q.addBindValue(path);
853
2/3
✓ Branch 21 → 22 taken 165 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 39 taken 165 times.
165 if (!q.exec()) {
854 ✗ qCWarning(lcCalStore) << "setCalendarSyncToken:" << q.lastError().text();
855 ✗ return false;
856 }
857
1/1
✓ Branch 39 → 40 taken 165 times.
165 return q.numRowsAffected() == 1;
858 165 }
859
860 12 QString CalendarStore::uidForHref(const QString &accountId, const QString &path,
861 const QString &href) const {
862 12 const qint64 calId = calendarIdForPath(accountId, path);
863
3/6
✓ Branch 3 → 4 taken 12 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 12 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12 times.
12 if (calId < 0 || href.isEmpty())
864 ✗ return {};
865
2/2
✓ Branch 49 → 12 taken 18 times.
✓ Branch 49 → 50 taken 4 times.
22 for (const auto *table : {"events", "tasks"}) {
866
1/1
✓ Branch 12 → 13 taken 18 times.
18 QSqlQuery q(m_db);
867
1/1
✓ Branch 19 → 20 taken 18 times.
54 q.prepare(QStringLiteral("SELECT uid FROM %1 WHERE calendar_id = ? AND resource_href = ? LIMIT 1")
868
1/1
✓ Branch 18 → 19 taken 18 times.
54 .arg(QLatin1String(table)));
869
1/1
✓ Branch 25 → 26 taken 18 times.
18 q.addBindValue(calId);
870
1/1
✓ Branch 29 → 30 taken 18 times.
18 q.addBindValue(href);
871
7/8
✓ Branch 31 → 32 taken 18 times.
✓ Branch 32 → 33 taken 18 times.
✗ Branch 32 → 36 not taken.
✓ Branch 33 → 34 taken 18 times.
✓ Branch 34 → 35 taken 8 times.
✓ Branch 34 → 36 taken 10 times.
✓ Branch 37 → 38 taken 8 times.
✓ Branch 37 → 43 taken 10 times.
18 if (q.exec() && q.next())
872
2/2
✓ Branch 38 → 39 taken 8 times.
✓ Branch 39 → 40 taken 8 times.
8 return q.value(0).toString();
873 18 }
874 4 return {};
875 }
876
877 6 bool CalendarStore::removeHref(const QString &accountId, const QString &path, const QString &href) {
878
1/1
✓ Branch 2 → 3 taken 6 times.
6 const qint64 calId = calendarIdForPath(accountId, path);
879
3/6
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 6 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 6 times.
6 if (calId < 0 || href.isEmpty())
880 ✗ return calId >= 0;
881
1/1
✓ Branch 10 → 11 taken 6 times.
6 const QString uid = uidForHref(accountId, path, href);
882
2/2
✓ Branch 56 → 13 taken 12 times.
✓ Branch 56 → 57 taken 6 times.
18 for (const auto *table : {"events", "tasks"}) {
883
1/1
✓ Branch 13 → 14 taken 12 times.
12 QSqlQuery q(m_db);
884
1/1
✓ Branch 20 → 21 taken 12 times.
36 q.prepare(QStringLiteral("DELETE FROM %1 WHERE calendar_id = ? AND resource_href = ?")
885
1/1
✓ Branch 19 → 20 taken 12 times.
36 .arg(QLatin1String(table)));
886
1/1
✓ Branch 26 → 27 taken 12 times.
12 q.addBindValue(calId);
887
1/1
✓ Branch 30 → 31 taken 12 times.
12 q.addBindValue(href);
888
2/3
✓ Branch 32 → 33 taken 12 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 50 taken 12 times.
12 if (!q.exec()) {
889 ✗ qCWarning(lcCalStore) << "removeHref:" << q.lastError().text();
890 ✗ return false;
891 }
892 12 }
893
1/1
✓ Branch 57 → 58 taken 6 times.
6 QSqlQuery resource(m_db);
894
1/1
✓ Branch 61 → 62 taken 6 times.
6 resource.prepare(QStringLiteral("DELETE FROM calendar_resources WHERE calendar_id = ? AND (href = ? OR uid = ?)"));
895
1/1
✓ Branch 66 → 67 taken 6 times.
6 resource.addBindValue(calId);
896
1/1
✓ Branch 70 → 71 taken 6 times.
6 resource.addBindValue(href);
897
1/1
✓ Branch 74 → 75 taken 6 times.
6 resource.addBindValue(uid);
898
2/3
✓ Branch 76 → 77 taken 6 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 94 taken 6 times.
6 if (!resource.exec()) {
899 ✗ qCWarning(lcCalStore) << "removeHref (resource):" << resource.lastError().text();
900 ✗ return false;
901 }
902 6 return true;
903 6 }
904
905 19808 QList<CalendarInfo> CalendarStore::allCalendars() const {
906 19808 QList<CalendarInfo> result;
907
1/1
✓ Branch 2 → 3 taken 19808 times.
19808 QSqlQuery q(m_db);
908
1/1
✓ Branch 6 → 7 taken 19808 times.
19808 q.exec(QStringLiteral(
909 "SELECT path, accountId, displayName, color, ctag, readOnly, supported_components "
910 "FROM calendars"));
911
3/3
✓ Branch 50 → 51 taken 51231 times.
✓ Branch 51 → 10 taken 31423 times.
✓ Branch 51 → 52 taken 19808 times.
51231 while (q.next()) {
912 31423 CalendarInfo cal;
913
2/2
✓ Branch 10 → 11 taken 31423 times.
✓ Branch 11 → 12 taken 31423 times.
31423 cal.path = q.value(0).toString();
914
2/2
✓ Branch 15 → 16 taken 31423 times.
✓ Branch 16 → 17 taken 31423 times.
31423 cal.accountId = q.value(1).toString();
915
2/2
✓ Branch 20 → 21 taken 31423 times.
✓ Branch 21 → 22 taken 31423 times.
31423 cal.displayName = q.value(2).toString();
916
2/2
✓ Branch 25 → 26 taken 31423 times.
✓ Branch 26 → 27 taken 31423 times.
31423 cal.color = q.value(3).toString();
917
2/2
✓ Branch 30 → 31 taken 31423 times.
✓ Branch 31 → 32 taken 31423 times.
31423 cal.ctag = q.value(4).toString();
918
2/2
✓ Branch 35 → 36 taken 31423 times.
✓ Branch 36 → 37 taken 31423 times.
31423 cal.readOnly = q.value(5).toBool();
919
3/3
✓ Branch 38 → 39 taken 31423 times.
✓ Branch 39 → 40 taken 31423 times.
✓ Branch 42 → 43 taken 31423 times.
31423 cal.supportedComponents = q.value(6).toString().split(',', Qt::SkipEmptyParts);
920
1/1
✓ Branch 47 → 48 taken 31423 times.
31423 result.append(cal);
921 31423 }
922 19808 return result;
923 19808 }
924
925 273 void CalendarStore::removeAccountCalendars(const QString &accountId) {
926
1/1
✓ Branch 2 → 3 taken 273 times.
273 QSqlQuery q(m_db);
927
1/1
✓ Branch 6 → 7 taken 273 times.
273 q.prepare(QStringLiteral("DELETE FROM calendars WHERE accountId = ?"));
928
1/1
✓ Branch 11 → 12 taken 273 times.
273 q.addBindValue(accountId);
929
2/3
✓ Branch 13 → 14 taken 273 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 31 taken 273 times.
273 if (!q.exec())
930 ✗ qCWarning(lcCalStore) << "removeAccountCalendars:" << q.lastError().text();
931 273 }
932
933 4 void CalendarStore::removeCalendarsNotIn(const QString &accountId,
934 const QStringList &activePaths) {
935 // S106-149: an empty list would delete every calendar of the account
936 // (with its events, tasks and resources) — the same hazard the sibling
937 // prunes refuse. Removing an account is removeAccountCalendars().
938
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 15 taken 3 times.
4 if (activePaths.isEmpty()) {
939
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCWarning(lcCalStore) << "removeCalendarsNotIn: refusing an empty list of active calendars";
940 1 return;
941 }
942 // Build placeholder string
943 3 QStringList placeholders;
944
2/2
✓ Branch 24 → 16 taken 4 times.
✓ Branch 24 → 25 taken 3 times.
7 for (int i = 0; i < activePaths.size(); ++i)
945
1/1
✓ Branch 19 → 20 taken 4 times.
4 placeholders << QStringLiteral("?");
946
947
1/1
✓ Branch 25 → 26 taken 3 times.
3 QSqlQuery q(m_db);
948
1/1
✓ Branch 35 → 36 taken 3 times.
9 q.prepare(QStringLiteral("DELETE FROM calendars WHERE accountId = ? "
949 "AND path NOT IN (%1)")
950
2/2
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
9 .arg(placeholders.join(QStringLiteral(","))));
951
1/1
✓ Branch 44 → 45 taken 3 times.
3 q.addBindValue(accountId);
952
2/2
✓ Branch 55 → 48 taken 4 times.
✓ Branch 55 → 56 taken 3 times.
7 for (const auto &p : activePaths)
953
1/1
✓ Branch 51 → 52 taken 4 times.
4 q.addBindValue(p);
954
1/1
✓ Branch 56 → 57 taken 3 times.
3 q.exec();
955 3 }
956
957 6 QString CalendarStore::accountIdForCalendarPath(const QString &path) const {
958
1/1
✓ Branch 2 → 3 taken 6 times.
6 QSqlQuery q(m_db);
959
1/1
✓ Branch 6 → 7 taken 6 times.
6 q.prepare(QStringLiteral("SELECT accountId FROM calendars WHERE path = ? "
960 "ORDER BY id LIMIT 2"));
961
1/1
✓ Branch 11 → 12 taken 6 times.
6 q.addBindValue(path);
962
8/8
✓ Branch 13 → 14 taken 6 times.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 17 taken 1 time.
✓ Branch 15 → 16 taken 5 times.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 4 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 4 times.
6 if (!q.exec() || !q.next())
963 2 return {};
964
2/2
✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
4 const QString accountId = q.value(0).toString();
965
3/3
✓ Branch 24 → 25 taken 4 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 38 taken 2 times.
4 if (q.next()) {
966
4/4
✓ Branch 26 → 27 taken 2 times.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 36 → 28 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
4 qCWarning(lcCalStore)
967
1/1
✓ Branch 31 → 32 taken 2 times.
2 << "accountIdForCalendarPath: ambiguous path without accountId:"
968
1/1
✓ Branch 32 → 33 taken 2 times.
2 << path;
969 2 return {};
970 }
971 2 return accountId;
972 6 }
973
974 68 void CalendarStore::setCalendarColor(const QString &path,
975 const QString &color) {
976
1/1
✓ Branch 2 → 3 taken 68 times.
68 const qint64 calId = calendarIdForPath(path);
977
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 64 times.
68 if (calId < 0)
978 4 return;
979
980
1/1
✓ Branch 5 → 6 taken 64 times.
64 QSqlQuery q(m_db);
981
1/1
✓ Branch 9 → 10 taken 64 times.
64 q.prepare(QStringLiteral("UPDATE calendars SET color = ? WHERE id = ?"));
982
1/1
✓ Branch 14 → 15 taken 64 times.
64 q.addBindValue(color);
983
1/1
✓ Branch 18 → 19 taken 64 times.
64 q.addBindValue(calId);
984
3/3
✓ Branch 20 → 21 taken 64 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 38 taken 63 times.
64 if (!q.exec())
985
8/8
✓ Branch 22 → 23 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 36 → 24 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
2 qCWarning(lcCalStore) << "setCalendarColor:" << q.lastError().text();
986 else
987
8/8
✓ Branch 38 → 39 taken 63 times.
✓ Branch 42 → 43 taken 63 times.
✓ Branch 43 → 44 taken 63 times.
✓ Branch 44 → 45 taken 63 times.
✓ Branch 45 → 46 taken 63 times.
✓ Branch 46 → 47 taken 63 times.
✓ Branch 53 → 40 taken 63 times.
✓ Branch 53 → 54 taken 63 times.
126 qCDebug(lcCalStore) << "setCalendarColor:" << path << "→" << color
988
3/3
✓ Branch 47 → 48 taken 63 times.
✓ Branch 48 → 49 taken 63 times.
✓ Branch 49 → 50 taken 63 times.
63 << "rows:" << q.numRowsAffected();
989 64 }
990
991 7 void CalendarStore::setCalendarColor(const QString &accountId,
992 const QString &path,
993 const QString &color) {
994
1/1
✓ Branch 2 → 3 taken 7 times.
7 QSqlQuery q(m_db);
995
1/1
✓ Branch 6 → 7 taken 7 times.
7 q.prepare(QStringLiteral(
996 "UPDATE calendars SET color = ? WHERE accountId = ? AND path = ?"));
997
1/1
✓ Branch 11 → 12 taken 7 times.
7 q.addBindValue(color);
998
1/1
✓ Branch 15 → 16 taken 7 times.
7 q.addBindValue(accountId);
999
1/1
✓ Branch 19 → 20 taken 7 times.
7 q.addBindValue(path);
1000
3/3
✓ Branch 21 → 22 taken 7 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 39 taken 6 times.
7 if (!q.exec())
1001
8/8
✓ Branch 23 → 24 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 37 → 25 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
2 qCWarning(lcCalStore) << "setCalendarColor:" << q.lastError().text();
1002 7 }
1003
1004 13 QString CalendarStore::calendarColor(const QString &path) const {
1005
1/1
✓ Branch 2 → 3 taken 13 times.
13 const qint64 calId = calendarIdForPath(path);
1006
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 17 taken 8 times.
13 if (calId < 0) {
1007
7/7
✓ Branch 4 → 5 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 10 → 11 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 15 → 6 taken 5 times.
✓ Branch 15 → 16 taken 5 times.
10 qCDebug(lcCalStore) << "calendarColor:" << path << "= (not found)";
1008 5 return {};
1009 }
1010
1011
1/1
✓ Branch 17 → 18 taken 8 times.
8 QSqlQuery q(m_db);
1012
1/1
✓ Branch 21 → 22 taken 8 times.
8 q.prepare(QStringLiteral("SELECT color FROM calendars WHERE id = ?"));
1013
1/1
✓ Branch 26 → 27 taken 8 times.
8 q.addBindValue(calId);
1014
8/8
✓ Branch 28 → 29 taken 8 times.
✓ Branch 29 → 30 taken 7 times.
✓ Branch 29 → 33 taken 1 time.
✓ Branch 30 → 31 taken 7 times.
✓ Branch 31 → 32 taken 6 times.
✓ Branch 31 → 33 taken 1 time.
✓ Branch 34 → 35 taken 6 times.
✓ Branch 34 → 54 taken 2 times.
8 if (q.exec() && q.next()) {
1015
2/2
✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 37 taken 6 times.
6 QString c = q.value(0).toString();
1016
8/8
✓ Branch 38 → 39 taken 6 times.
✓ Branch 42 → 43 taken 6 times.
✓ Branch 43 → 44 taken 6 times.
✓ Branch 44 → 45 taken 6 times.
✓ Branch 45 → 46 taken 6 times.
✓ Branch 46 → 47 taken 6 times.
✓ Branch 50 → 40 taken 6 times.
✓ Branch 50 → 51 taken 6 times.
12 qCDebug(lcCalStore) << "calendarColor:" << path << "=" << c;
1017 6 return c;
1018 6 }
1019
7/7
✓ Branch 54 → 55 taken 2 times.
✓ Branch 58 → 59 taken 2 times.
✓ Branch 59 → 60 taken 2 times.
✓ Branch 60 → 61 taken 2 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 65 → 56 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
4 qCDebug(lcCalStore) << "calendarColor:" << path << "= (not found)";
1020 2 return {};
1021 8 }
1022
1023 538 QString CalendarStore::calendarColor(const QString &accountId,
1024 const QString &path) const {
1025
1/1
✓ Branch 2 → 3 taken 538 times.
538 QSqlQuery q(m_db);
1026
1/1
✓ Branch 6 → 7 taken 538 times.
538 q.prepare(QStringLiteral(
1027 "SELECT color FROM calendars WHERE accountId = ? AND path = ?"));
1028
1/1
✓ Branch 11 → 12 taken 538 times.
538 q.addBindValue(accountId);
1029
1/1
✓ Branch 15 → 16 taken 538 times.
538 q.addBindValue(path);
1030
8/8
✓ Branch 17 → 18 taken 538 times.
✓ Branch 18 → 19 taken 536 times.
✓ Branch 18 → 22 taken 2 times.
✓ Branch 19 → 20 taken 536 times.
✓ Branch 20 → 21 taken 96 times.
✓ Branch 20 → 22 taken 440 times.
✓ Branch 23 → 24 taken 96 times.
✓ Branch 23 → 29 taken 442 times.
538 if (q.exec() && q.next())
1031
2/2
✓ Branch 24 → 25 taken 96 times.
✓ Branch 25 → 26 taken 96 times.
96 return q.value(0).toString();
1032 442 return {};
1033 538 }
1034
1035 430 bool CalendarStore::beginTransaction() {
1036
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 429 times.
430 if (m_inTransaction) return true;
1037 429 m_inTransaction = beginImmediate(m_db);
1038 429 return m_inTransaction;
1039 }
1040
1041 428 bool CalendarStore::commitTransaction() {
1042
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 426 times.
428 if (!m_inTransaction) return false;
1043 426 const bool ok = m_db.commit();
1044
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 426 times.
426 if (!ok) m_db.rollback();
1045 426 m_inTransaction = false;
1046 426 return ok;
1047 }
1048
1049 3 void CalendarStore::rollbackTransaction() {
1050
1/2
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
3 if (m_inTransaction) m_db.rollback();
1051 3 m_inTransaction = false;
1052 3 }
1053
1054 // ═══════════════════════════════════════════════════════
1055 // Events
1056 // ═══════════════════════════════════════════════════════
1057
1058 // T-79.C1/H3: EXDATE/RDATE list ↔ TEXT column (comma-joined ISO-8601 UTC).
1059 66416 static QString serializeDateList(const QList<QDateTime> &values) {
1060 66416 QStringList parts;
1061
2/2
✓ Branch 15 → 4 taken 72 times.
✓ Branch 15 → 16 taken 66416 times.
66488 for (const QDateTime &dt : values)
1062
2/3
✓ Branch 5 → 6 taken 72 times.
✓ Branch 6 → 7 taken 72 times.
✗ Branch 6 → 13 not taken.
72 if (dt.isValid())
1063
3/3
✓ Branch 7 → 8 taken 72 times.
✓ Branch 8 → 9 taken 72 times.
✓ Branch 9 → 10 taken 72 times.
72 parts << dt.toUTC().toString(Qt::ISODate);
1064
1/1
✓ Branch 18 → 19 taken 66416 times.
132832 return parts.join(QLatin1Char(','));
1065 66416 }
1066
1067 1792711 static QList<QDateTime> deserializeDateList(const QString &text) {
1068 1792711 QList<QDateTime> result;
1069
1/1
✓ Branch 5 → 6 taken 1792711 times.
1792711 const QStringList parts = text.split(QLatin1Char(','), Qt::SkipEmptyParts);
1070
2/2
✓ Branch 16 → 8 taken 313 times.
✓ Branch 16 → 17 taken 1792711 times.
1793024 for (const QString &part : parts) {
1071
1/1
✓ Branch 9 → 10 taken 313 times.
313 QDateTime dt = QDateTime::fromString(part, Qt::ISODate);
1072
2/3
✓ Branch 10 → 11 taken 313 times.
✓ Branch 11 → 12 taken 313 times.
✗ Branch 11 → 13 not taken.
313 if (dt.isValid())
1073
1/1
✓ Branch 12 → 13 taken 313 times.
313 result.append(dt);
1074 313 }
1075 1792711 return result;
1076 1792711 }
1077
1078 2025 static QString serializeReminders(const QList<int> &minutes) {
1079 2025 QStringList parts;
1080
4/4
✓ Branch 5 → 6 taken 44 times.
✓ Branch 6 → 7 taken 44 times.
✓ Branch 10 → 4 taken 44 times.
✓ Branch 10 → 11 taken 2025 times.
2069 for (const int value : minutes) parts << QString::number(value);
1081
1/1
✓ Branch 13 → 14 taken 2025 times.
4050 return parts.join(QLatin1Char(','));
1082 2025 }
1083
1084 8283 static QList<int> deserializeReminders(const QString &text) {
1085 8283 QList<int> minutes;
1086
5/5
✓ Branch 5 → 6 taken 8283 times.
✓ Branch 6 → 7 taken 8283 times.
✓ Branch 7 → 8 taken 8283 times.
✓ Branch 20 → 9 taken 374 times.
✓ Branch 20 → 21 taken 8283 times.
8657 for (const QString &part : text.split(QLatin1Char(','), Qt::SkipEmptyParts)) {
1087 374 bool ok = false;
1088
1/1
✓ Branch 10 → 11 taken 374 times.
374 const int value = part.toInt(&ok);
1089
4/7
✓ Branch 11 → 12 taken 374 times.
✗ Branch 11 → 15 not taken.
✓ Branch 13 → 14 taken 374 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 374 times.
✗ Branch 16 → 18 not taken.
✓ Branch 17 → 18 taken 374 times.
374 if (ok && !minutes.contains(value)) minutes.append(value);
1090 8283 }
1091
3/3
✓ Branch 22 → 23 taken 8283 times.
✓ Branch 23 → 24 taken 8283 times.
✓ Branch 24 → 25 taken 8283 times.
8283 std::sort(minutes.begin(), minutes.end());
1092 8283 return minutes;
1093 ✗ }
1094
1095 // Column list shared by every event query (indices used by eventFromRow).
1096 // Sprint 93: raw component and resource text only for write-back queries.
1097 6797 static QString eventColumns(bool withRaw) {
1098 6797 QString columns = QStringLiteral(
1099 "e.id, e.uid, c.path, c.accountId, e.summary, e.description, e.location, "
1100 "e.dt_start, e.dt_end, e.all_day, e.rrule, e.etag, e.last_modified, c.color, "
1101 "e.resource_href, e.exdates, e.recurrence_id, e.tz_start, e.tz_end, e.rdates, "
1102 "COALESCE(o.state, ''), COALESCE(e.reminders, '')");
1103
2/2
✓ Branch 6 → 7 taken 1702 times.
✓ Branch 6 → 14 taken 5095 times.
6797 if (withRaw)
1104
1/1
✓ Branch 10 → 11 taken 1702 times.
1702 columns += QStringLiteral(", e.ical_raw, r.ical");
1105 6797 return columns;
1106 ✗ }
1107
1108 6797 static QString eventFrom(bool withRaw) {
1109 6797 QString from = QStringLiteral(
1110 " FROM events e JOIN calendars c ON e.calendar_id = c.id "
1111 "LEFT JOIN calendar_outbox o ON o.calendar_id = e.calendar_id AND o.uid = e.uid ");
1112
2/2
✓ Branch 6 → 7 taken 1702 times.
✓ Branch 6 → 14 taken 5095 times.
6797 if (withRaw)
1113
1/1
✓ Branch 10 → 11 taken 1702 times.
1702 from += QStringLiteral("LEFT JOIN calendar_resources r "
1114 "ON r.calendar_id = e.calendar_id AND r.uid = e.uid ");
1115 6797 return from;
1116 ✗ }
1117
1118 8283 static CalendarEvent eventFromRow(const QSqlQuery &q, bool withRaw) {
1119 8283 CalendarEvent ev;
1120
2/2
✓ Branch 3 → 4 taken 8283 times.
✓ Branch 4 → 5 taken 8283 times.
8283 ev.id = q.value(0).toLongLong();
1121
2/2
✓ Branch 6 → 7 taken 8283 times.
✓ Branch 7 → 8 taken 8283 times.
8283 ev.uid = q.value(1).toString();
1122
2/2
✓ Branch 11 → 12 taken 8283 times.
✓ Branch 12 → 13 taken 8283 times.
8283 ev.calendarPath = q.value(2).toString();
1123
2/2
✓ Branch 16 → 17 taken 8283 times.
✓ Branch 17 → 18 taken 8283 times.
8283 ev.accountId = q.value(3).toString();
1124
2/2
✓ Branch 21 → 22 taken 8283 times.
✓ Branch 22 → 23 taken 8283 times.
8283 ev.summary = q.value(4).toString();
1125
2/2
✓ Branch 26 → 27 taken 8283 times.
✓ Branch 27 → 28 taken 8283 times.
8283 ev.description = q.value(5).toString();
1126
2/2
✓ Branch 31 → 32 taken 8283 times.
✓ Branch 32 → 33 taken 8283 times.
8283 ev.location = q.value(6).toString();
1127
2/2
✓ Branch 36 → 37 taken 8283 times.
✓ Branch 37 → 38 taken 8283 times.
8283 ev.allDay = q.value(9).toBool();
1128 // Sprint 93 S93.1.2: the instant comes back in its stored zone so the
1129 // expansion steps on the event's wall clock.
1130
6/8
✓ Branch 39 → 40 taken 466 times.
✓ Branch 39 → 41 taken 7817 times.
✓ Branch 41 → 42 taken 7817 times.
✓ Branch 42 → 43 taken 7817 times.
✓ Branch 43 → 44 taken 7817 times.
✓ Branch 43 → 45 taken 466 times.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
8283 const QString startZone = ev.allDay ? QString() : q.value(17).toString();
1131
6/8
✓ Branch 45 → 46 taken 466 times.
✓ Branch 45 → 47 taken 7817 times.
✓ Branch 47 → 48 taken 7817 times.
✓ Branch 48 → 49 taken 7817 times.
✓ Branch 49 → 50 taken 7817 times.
✓ Branch 49 → 51 taken 466 times.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 194 not taken.
8283 const QString endZone = ev.allDay ? QString() : q.value(18).toString();
1132
4/4
✓ Branch 51 → 52 taken 8283 times.
✓ Branch 52 → 53 taken 8283 times.
✓ Branch 54 → 55 taken 8280 times.
✓ Branch 54 → 62 taken 3 times.
8283 if (!q.value(7).isNull())
1133
3/3
✓ Branch 55 → 56 taken 8280 times.
✓ Branch 56 → 57 taken 8280 times.
✓ Branch 57 → 58 taken 8280 times.
8280 ev.dtStart = ICalTime::fromStored(q.value(7).toLongLong(), startZone);
1134
4/4
✓ Branch 62 → 63 taken 8283 times.
✓ Branch 63 → 64 taken 8283 times.
✓ Branch 65 → 66 taken 8252 times.
✓ Branch 65 → 73 taken 31 times.
8283 if (!q.value(8).isNull())
1135
3/3
✓ Branch 66 → 67 taken 8252 times.
✓ Branch 67 → 68 taken 8252 times.
✓ Branch 68 → 69 taken 8252 times.
8252 ev.dtEnd = ICalTime::fromStored(q.value(8).toLongLong(), endZone);
1136
2/2
✓ Branch 73 → 74 taken 8283 times.
✓ Branch 74 → 75 taken 8283 times.
8283 ev.rrule = q.value(10).toString();
1137
2/2
✓ Branch 78 → 79 taken 8283 times.
✓ Branch 79 → 80 taken 8283 times.
8283 ev.etag = q.value(11).toString();
1138
4/4
✓ Branch 83 → 84 taken 8283 times.
✓ Branch 84 → 85 taken 8283 times.
✓ Branch 86 → 87 taken 1710 times.
✓ Branch 86 → 96 taken 6573 times.
8283 if (!q.value(12).isNull())
1139
4/4
✓ Branch 87 → 88 taken 1710 times.
✓ Branch 88 → 89 taken 1710 times.
✓ Branch 89 → 90 taken 1710 times.
✓ Branch 90 → 91 taken 1710 times.
1710 ev.lastModified = QDateTime::fromSecsSinceEpoch(q.value(12).toLongLong(), QTimeZone::utc());
1140
2/2
✓ Branch 96 → 97 taken 8283 times.
✓ Branch 97 → 98 taken 8283 times.
8283 ev.color = q.value(13).toString();
1141
2/2
✓ Branch 101 → 102 taken 8283 times.
✓ Branch 102 → 103 taken 8283 times.
8283 ev.resourceHref = q.value(14).toString();
1142
3/3
✓ Branch 106 → 107 taken 8283 times.
✓ Branch 107 → 108 taken 8283 times.
✓ Branch 108 → 109 taken 8283 times.
8283 ev.exdates = deserializeDateList(q.value(15).toString());
1143
2/2
✓ Branch 113 → 114 taken 8283 times.
✓ Branch 114 → 115 taken 8283 times.
8283 const QString recurrenceId = q.value(16).toString();
1144
2/2
✓ Branch 117 → 118 taken 52 times.
✓ Branch 117 → 122 taken 8231 times.
8283 if (!recurrenceId.isEmpty())
1145
1/1
✓ Branch 118 → 119 taken 52 times.
52 ev.recurrenceId = QDateTime::fromString(recurrenceId, Qt::ISODate);
1146
3/3
✓ Branch 122 → 123 taken 8283 times.
✓ Branch 123 → 124 taken 8283 times.
✓ Branch 124 → 125 taken 8283 times.
8283 ev.rdates = deserializeDateList(q.value(19).toString());
1147
2/2
✓ Branch 129 → 130 taken 8283 times.
✓ Branch 130 → 131 taken 8283 times.
8283 ev.syncState = q.value(20).toString();
1148
3/3
✓ Branch 134 → 135 taken 8283 times.
✓ Branch 135 → 136 taken 8283 times.
✓ Branch 136 → 137 taken 8283 times.
8283 ev.reminders = deserializeReminders(q.value(21).toString());
1149
2/2
✓ Branch 141 → 142 taken 1900 times.
✓ Branch 141 → 153 taken 6383 times.
8283 if (withRaw) {
1150
2/2
✓ Branch 142 → 143 taken 1900 times.
✓ Branch 143 → 144 taken 1900 times.
1900 ev.icalRaw = q.value(22).toString();
1151
2/2
✓ Branch 147 → 148 taken 1900 times.
✓ Branch 148 → 149 taken 1900 times.
1900 ev.resourceIcal = q.value(23).toString();
1152 }
1153 8283 return ev;
1154 8283 }
1155
1156 // S106-139: the event row, its people index and its resource are one
1157 // write unit — joined to the caller's transaction, or its own. A failed
1158 // resource row used to leave the edit stored without an outbox entry.
1159 2029 bool CalendarStore::upsertEvent(const CalendarEvent &event) {
1160
1/1
✓ Branch 3 → 4 taken 2029 times.
4058 return inSavepoint([&] { return writeEvent(event); });
1161 }
1162
1163 62368 bool CalendarStore::upsertTask(const CalendarTask &task) {
1164
1/1
✓ Branch 3 → 4 taken 62368 times.
124736 return inSavepoint([&] { return writeTask(task); });
1165 }
1166
1167 // A SQLite savepoint nests inside whatever transaction the caller holds
1168 // (beginTransaction(), moveTask(), a migration); a failed write is rolled
1169 // back to it. Without one, the write is its own IMMEDIATE transaction (a
1170 // deferred one — a bare SAVEPOINT — fails at once on lock contention,
1171 // see beginImmediate()).
1172 64397 bool CalendarStore::inSavepoint(const std::function<bool()> &write) {
1173
1/1
✓ Branch 2 → 3 taken 64397 times.
64397 QSqlQuery q(m_db);
1174 // m_inTransaction covers every transaction this store opens around its
1175 // writes (beginTransaction(), moveTask()); migrations write no rows
1176 // through here.
1177
2/2
✓ Branch 3 → 4 taken 1823 times.
✓ Branch 3 → 29 taken 62574 times.
64397 if (!m_inTransaction) {
1178
2/3
✓ Branch 4 → 5 taken 1823 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1823 times.
1823 if (!beginImmediate(m_db))
1179 ✗ return false;
1180
11/16
✓ Branch 7 → 8 taken 1823 times.
✓ Branch 8 → 9 taken 1812 times.
✓ Branch 8 → 15 taken 11 times.
✓ Branch 12 → 13 taken 1812 times.
✓ Branch 13 → 14 taken 1812 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 1812 times.
✓ Branch 16 → 18 taken 11 times.
✓ Branch 18 → 19 taken 1812 times.
✓ Branch 18 → 20 taken 11 times.
✓ Branch 20 → 21 taken 1812 times.
✓ Branch 20 → 22 taken 11 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 89 not taken.
3635 if (write() && q.exec(QStringLiteral("COMMIT")))
1181 1812 return true;
1182
1/1
✓ Branch 25 → 26 taken 11 times.
11 q.exec(QStringLiteral("ROLLBACK"));
1183 11 return false;
1184 }
1185
2/3
✓ Branch 32 → 33 taken 62574 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 52 taken 62574 times.
62574 if (!q.exec(QStringLiteral("SAVEPOINT calendar_write"))) {
1186 ✗ qCWarning(lcCalStore) << "savepoint:" << q.lastError().text();
1187 ✗ return false;
1188 }
1189
11/16
✓ Branch 52 → 53 taken 62574 times.
✓ Branch 53 → 54 taken 62572 times.
✓ Branch 53 → 60 taken 2 times.
✓ Branch 57 → 58 taken 62572 times.
✓ Branch 58 → 59 taken 62572 times.
✗ Branch 58 → 60 not taken.
✓ Branch 61 → 62 taken 62572 times.
✓ Branch 61 → 63 taken 2 times.
✓ Branch 63 → 64 taken 62572 times.
✓ Branch 63 → 65 taken 2 times.
✓ Branch 65 → 66 taken 62572 times.
✓ Branch 65 → 67 taken 2 times.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 116 not taken.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
125146 if (write() && q.exec(QStringLiteral("RELEASE calendar_write")))
1190 62572 return true;
1191
1/1
✓ Branch 70 → 71 taken 2 times.
2 q.exec(QStringLiteral("ROLLBACK TO calendar_write"));
1192
1/1
✓ Branch 76 → 77 taken 2 times.
2 q.exec(QStringLiteral("RELEASE calendar_write"));
1193 2 return false;
1194 64397 }
1195
1196 2029 bool CalendarStore::writeEvent(const CalendarEvent &event) {
1197
1/1
✓ Branch 2 → 3 taken 2029 times.
2029 qint64 calId = calendarIdForPath(event.accountId, event.calendarPath);
1198
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 17 taken 2025 times.
2029 if (calId < 0) {
1199
5/5
✓ Branch 4 → 5 taken 4 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 15 → 6 taken 4 times.
✓ Branch 15 → 16 taken 4 times.
8 qCWarning(lcCalStore) << "upsertEvent: calendar not found:"
1200
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
4 << event.accountId << event.calendarPath;
1201 4 return false;
1202 }
1203
1204
1/1
✓ Branch 17 → 18 taken 2025 times.
2025 QSqlQuery q(m_db);
1205
1/1
✓ Branch 21 → 22 taken 2025 times.
2025 q.prepare(QStringLiteral(
1206 "INSERT INTO events (calendar_id, uid, summary, description, location, "
1207 " dt_start, dt_end, all_day, rrule, etag, last_modified, resource_href, "
1208 " exdates, recurrence_id, tz_start, tz_end, rdates, ical_raw, reminders, series_end) "
1209 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
1210 "ON CONFLICT(calendar_id, uid, recurrence_id) DO UPDATE SET "
1211 " summary = excluded.summary,"
1212 " description = excluded.description,"
1213 " location = excluded.location,"
1214 " dt_start = excluded.dt_start,"
1215 " dt_end = excluded.dt_end,"
1216 " all_day = excluded.all_day,"
1217 " rrule = excluded.rrule,"
1218 " etag = excluded.etag,"
1219 " last_modified = excluded.last_modified,"
1220 " resource_href = excluded.resource_href,"
1221 " exdates = excluded.exdates,"
1222 " tz_start = excluded.tz_start,"
1223 " tz_end = excluded.tz_end,"
1224 " rdates = excluded.rdates,"
1225 " ical_raw = excluded.ical_raw,"
1226 " reminders = excluded.reminders,"
1227 " series_end = excluded.series_end"));
1228
1/1
✓ Branch 26 → 27 taken 2025 times.
2025 q.addBindValue(calId);
1229
1/1
✓ Branch 30 → 31 taken 2025 times.
2025 q.addBindValue(event.uid);
1230
1/1
✓ Branch 34 → 35 taken 2025 times.
2025 q.addBindValue(event.summary);
1231
1/1
✓ Branch 38 → 39 taken 2025 times.
2025 q.addBindValue(event.description);
1232
1/1
✓ Branch 42 → 43 taken 2025 times.
2025 q.addBindValue(event.location);
1233
5/5
✓ Branch 45 → 46 taken 2025 times.
✓ Branch 46 → 47 taken 2019 times.
✓ Branch 46 → 49 taken 6 times.
✓ Branch 47 → 48 taken 2019 times.
✓ Branch 50 → 51 taken 2025 times.
2025 q.addBindValue(event.dtStart.isValid() ? event.dtStart.toSecsSinceEpoch()
1234 : QVariant());
1235
5/5
✓ Branch 53 → 54 taken 2025 times.
✓ Branch 54 → 55 taken 1999 times.
✓ Branch 54 → 57 taken 26 times.
✓ Branch 55 → 56 taken 1999 times.
✓ Branch 58 → 59 taken 2025 times.
2025 q.addBindValue(event.dtEnd.isValid() ? event.dtEnd.toSecsSinceEpoch()
1236 : QVariant());
1237
3/3
✓ Branch 61 → 62 taken 84 times.
✓ Branch 61 → 63 taken 1941 times.
✓ Branch 65 → 66 taken 2025 times.
2025 q.addBindValue(event.allDay ? 1 : 0);
1238
1/1
✓ Branch 69 → 70 taken 2025 times.
2025 q.addBindValue(event.rrule);
1239
1/1
✓ Branch 73 → 74 taken 2025 times.
2025 q.addBindValue(event.etag);
1240
2/2
✓ Branch 76 → 77 taken 2025 times.
✓ Branch 81 → 82 taken 2025 times.
4050 q.addBindValue(event.lastModified.isValid()
1241
3/3
✓ Branch 77 → 78 taken 778 times.
✓ Branch 77 → 80 taken 1247 times.
✓ Branch 78 → 79 taken 778 times.
4050 ? event.lastModified.toSecsSinceEpoch()
1242 : QVariant());
1243
1/1
✓ Branch 85 → 86 taken 2025 times.
2025 q.addBindValue(event.resourceHref);
1244
2/2
✓ Branch 88 → 89 taken 2025 times.
✓ Branch 90 → 91 taken 2025 times.
2025 q.addBindValue(serializeDateList(event.exdates));
1245 // Masters carry the empty string (never NULL): SQLite treats NULLs as
1246 // distinct in the unique index, which would duplicate master rows.
1247
2/2
✓ Branch 94 → 95 taken 2025 times.
✓ Branch 102 → 103 taken 2025 times.
4050 q.addBindValue(event.recurrenceId.isValid()
1248
4/6
✓ Branch 95 → 96 taken 10 times.
✓ Branch 95 → 98 taken 2015 times.
✓ Branch 96 → 97 taken 10 times.
✓ Branch 97 → 101 taken 10 times.
✗ Branch 326 → 327 not taken.
✗ Branch 326 → 328 not taken.
8090 ? event.recurrenceId.toUTC().toString(Qt::ISODate)
1249
4/6
✓ Branch 105 → 106 taken 2015 times.
✓ Branch 105 → 107 taken 10 times.
✓ Branch 107 → 108 taken 10 times.
✓ Branch 107 → 109 taken 2015 times.
✗ Branch 322 → 323 not taken.
✗ Branch 322 → 324 not taken.
2025 : QStringLiteral(""));
1250
2/2
✓ Branch 110 → 111 taken 2025 times.
✓ Branch 112 → 113 taken 2025 times.
2025 q.addBindValue(ICalTime::zoneId(event.dtStart, event.allDay));
1251
2/2
✓ Branch 116 → 117 taken 2025 times.
✓ Branch 118 → 119 taken 2025 times.
2025 q.addBindValue(ICalTime::zoneId(event.dtEnd, event.allDay));
1252
2/2
✓ Branch 122 → 123 taken 2025 times.
✓ Branch 124 → 125 taken 2025 times.
2025 q.addBindValue(serializeDateList(event.rdates));
1253
3/3
✓ Branch 129 → 130 taken 1276 times.
✓ Branch 129 → 131 taken 749 times.
✓ Branch 132 → 133 taken 2025 times.
2025 q.addBindValue(event.icalRaw.isEmpty() ? QVariant() : QVariant(event.icalRaw));
1254
2/2
✓ Branch 135 → 136 taken 2025 times.
✓ Branch 137 → 138 taken 2025 times.
2025 q.addBindValue(serializeReminders(event.reminders));
1255
1/1
✓ Branch 140 → 141 taken 2025 times.
2025 const QDateTime seriesEndValue = seriesEnd(event);
1256
5/5
✓ Branch 142 → 143 taken 2025 times.
✓ Branch 143 → 144 taken 1986 times.
✓ Branch 143 → 146 taken 39 times.
✓ Branch 144 → 145 taken 1986 times.
✓ Branch 147 → 148 taken 2025 times.
2025 q.addBindValue(seriesEndValue.isValid() ? QVariant(seriesEndValue.toSecsSinceEpoch()) : QVariant());
1257
3/3
✓ Branch 149 → 150 taken 2025 times.
✓ Branch 150 → 151 taken 2 times.
✓ Branch 150 → 167 taken 2023 times.
2025 if (!q.exec()) {
1258
8/8
✓ Branch 151 → 152 taken 2 times.
✓ Branch 155 → 156 taken 2 times.
✓ Branch 156 → 157 taken 2 times.
✓ Branch 157 → 158 taken 2 times.
✓ Branch 158 → 159 taken 2 times.
✓ Branch 159 → 160 taken 2 times.
✓ Branch 165 → 153 taken 2 times.
✓ Branch 165 → 166 taken 2 times.
4 qCWarning(lcCalStore) << "upsertEvent:" << q.lastError().text();
1259 2 return false;
1260 }
1261
1/1
✓ Branch 167 → 168 taken 2023 times.
2023 QSqlQuery idQuery(m_db);
1262
1/1
✓ Branch 171 → 172 taken 2023 times.
2023 idQuery.prepare(QStringLiteral("SELECT id FROM events WHERE calendar_id = ? AND uid = ? AND recurrence_id = ?"));
1263
1/1
✓ Branch 176 → 177 taken 2023 times.
2023 idQuery.addBindValue(calId);
1264
1/1
✓ Branch 180 → 181 taken 2023 times.
2023 idQuery.addBindValue(event.uid);
1265
6/8
✓ Branch 183 → 184 taken 2023 times.
✓ Branch 184 → 185 taken 10 times.
✓ Branch 184 → 187 taken 2013 times.
✓ Branch 185 → 186 taken 10 times.
✓ Branch 186 → 190 taken 10 times.
✓ Branch 191 → 192 taken 2023 times.
✗ Branch 402 → 403 not taken.
✗ Branch 402 → 404 not taken.
4046 idQuery.addBindValue(event.recurrenceId.isValid() ? event.recurrenceId.toUTC().toString(Qt::ISODate)
1266
4/6
✓ Branch 194 → 195 taken 2013 times.
✓ Branch 194 → 196 taken 10 times.
✓ Branch 196 → 197 taken 10 times.
✓ Branch 196 → 198 taken 2013 times.
✗ Branch 398 → 399 not taken.
✗ Branch 398 → 400 not taken.
2023 : QStringLiteral(""));
1267 // S106-141: a failed people index write is an error of the save, not a
1268 // silent gap in the contact history.
1269
5/8
✓ Branch 198 → 199 taken 2023 times.
✓ Branch 199 → 200 taken 2023 times.
✗ Branch 199 → 202 not taken.
✓ Branch 200 → 201 taken 2023 times.
✗ Branch 201 → 202 not taken.
✓ Branch 201 → 203 taken 2023 times.
✗ Branch 204 → 205 not taken.
✓ Branch 204 → 221 taken 2023 times.
2023 if (!idQuery.exec() || !idQuery.next()) {
1270 ✗ qCWarning(lcCalStore) << "upsertEvent: row id:" << idQuery.lastError().text();
1271 ✗ return false;
1272 }
1273
5/5
✓ Branch 221 → 222 taken 2023 times.
✓ Branch 222 → 223 taken 2023 times.
✓ Branch 223 → 224 taken 2023 times.
✓ Branch 225 → 226 taken 1 time.
✓ Branch 225 → 237 taken 2022 times.
2023 if (!CalendarPeopleIndex::write(m_db, idQuery.value(0).toLongLong(), event)) {
1274
5/5
✓ Branch 226 → 227 taken 1 time.
✓ Branch 230 → 231 taken 1 time.
✓ Branch 231 → 232 taken 1 time.
✓ Branch 235 → 228 taken 1 time.
✓ Branch 235 → 236 taken 1 time.
2 qCWarning(lcCalStore) << "upsertEvent: people index write failed";
1275 1 return false;
1276 }
1277
7/7
✓ Branch 238 → 239 taken 728 times.
✓ Branch 238 → 242 taken 1294 times.
✓ Branch 239 → 240 taken 728 times.
✓ Branch 240 → 241 taken 723 times.
✓ Branch 240 → 242 taken 5 times.
✓ Branch 243 → 244 taken 723 times.
✓ Branch 243 → 246 taken 1299 times.
2022 if (!event.resourceIcal.isEmpty() && !event.recurrenceId.isValid())
1278 1446 return upsertResource(event.accountId, event.calendarPath, event.uid,
1279
1/1
✓ Branch 244 → 245 taken 723 times.
723 event.resourceHref, event.etag, event.resourceIcal);
1280 1299 return true;
1281 2025 }
1282
1283 1235 bool CalendarStore::upsertResource(const QString &accountId, const QString &calendarPath,
1284 const QString &uid, const QString &href,
1285 const QString &etag, const QString &ical) {
1286
1/1
✓ Branch 2 → 3 taken 1235 times.
1235 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1287
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1235 times.
1235 if (calId < 0)
1288 ✗ return false;
1289
1/1
✓ Branch 5 → 6 taken 1235 times.
1235 QSqlQuery q(m_db);
1290
1/1
✓ Branch 9 → 10 taken 1235 times.
1235 q.prepare(QStringLiteral(
1291 "INSERT INTO calendar_resources (calendar_id, uid, href, etag, ical) VALUES (?, ?, ?, ?, ?) "
1292 "ON CONFLICT(calendar_id, uid) DO UPDATE SET href = excluded.href, "
1293 "etag = excluded.etag, ical = excluded.ical"));
1294
1/1
✓ Branch 14 → 15 taken 1235 times.
1235 q.addBindValue(calId);
1295
1/1
✓ Branch 18 → 19 taken 1235 times.
1235 q.addBindValue(uid);
1296
1/1
✓ Branch 22 → 23 taken 1235 times.
1235 q.addBindValue(href);
1297
1/1
✓ Branch 26 → 27 taken 1235 times.
1235 q.addBindValue(etag);
1298
1/1
✓ Branch 30 → 31 taken 1235 times.
1235 q.addBindValue(ical);
1299
3/3
✓ Branch 32 → 33 taken 1235 times.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 50 taken 1234 times.
1235 if (!q.exec()) {
1300
8/8
✓ Branch 34 → 35 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 48 → 36 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
2 qCWarning(lcCalStore) << "upsertResource:" << q.lastError().text();
1301 1 return false;
1302 }
1303 1234 return true;
1304 1235 }
1305
1306 221 QString CalendarStore::resourceFor(const QString &accountId, const QString &calendarPath,
1307 const QString &uid) const {
1308
1/1
✓ Branch 2 → 3 taken 221 times.
221 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1309
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 221 times.
221 if (calId < 0)
1310 ✗ return {};
1311
1/1
✓ Branch 5 → 6 taken 221 times.
221 QSqlQuery q(m_db);
1312
1/1
✓ Branch 9 → 10 taken 221 times.
221 q.prepare(QStringLiteral("SELECT ical FROM calendar_resources WHERE calendar_id = ? AND uid = ?"));
1313
1/1
✓ Branch 14 → 15 taken 221 times.
221 q.addBindValue(calId);
1314
1/1
✓ Branch 18 → 19 taken 221 times.
221 q.addBindValue(uid);
1315
7/8
✓ Branch 20 → 21 taken 221 times.
✓ Branch 21 → 22 taken 221 times.
✗ Branch 21 → 25 not taken.
✓ Branch 22 → 23 taken 221 times.
✓ Branch 23 → 24 taken 193 times.
✓ Branch 23 → 25 taken 28 times.
✓ Branch 26 → 27 taken 193 times.
✓ Branch 26 → 32 taken 28 times.
221 if (q.exec() && q.next())
1316
2/2
✓ Branch 27 → 28 taken 193 times.
✓ Branch 28 → 29 taken 193 times.
193 return q.value(0).toString();
1317 28 return {};
1318 221 }
1319
1320 293 bool CalendarStore::backfillSeriesEnds() {
1321
1/1
✓ Branch 2 → 3 taken 293 times.
293 QSqlQuery rows(m_db);
1322
5/5
✓ Branch 6 → 7 taken 293 times.
✓ Branch 7 → 8 taken 293 times.
✓ Branch 11 → 12 taken 293 times.
✓ Branch 12 → 13 taken 293 times.
✓ Branch 14 → 15 taken 293 times.
879 if (!rows.exec(QStringLiteral("SELECT ") + eventColumns(false) + eventFrom(false) +
1323
2/3
✓ Branch 13 → 14 taken 293 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 293 times.
879 QStringLiteral("WHERE COALESCE(e.rrule, '') != '' OR COALESCE(e.rdates, '') != ''")))
1324 ✗ return false;
1325
1/1
✓ Branch 26 → 27 taken 293 times.
293 QSqlQuery update(m_db);
1326
1/1
✓ Branch 30 → 31 taken 293 times.
293 update.prepare(QStringLiteral("UPDATE events SET series_end = ? WHERE id = ?"));
1327 293 int filled = 0;
1328
3/3
✓ Branch 63 → 64 taken 297 times.
✓ Branch 64 → 34 taken 4 times.
✓ Branch 64 → 65 taken 293 times.
297 while (rows.next()) {
1329
1/1
✓ Branch 34 → 35 taken 4 times.
4 const CalendarEvent event = eventFromRow(rows, false);
1330
1/1
✓ Branch 35 → 36 taken 4 times.
4 const QDateTime end = seriesEnd(event);
1331
3/3
✓ Branch 36 → 37 taken 4 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 3 times.
4 if (!end.isValid()) continue; // unbounded: stays NULL
1332
2/2
✓ Branch 40 → 41 taken 3 times.
✓ Branch 42 → 43 taken 3 times.
3 update.addBindValue(end.toSecsSinceEpoch());
1333
1/1
✓ Branch 46 → 47 taken 3 times.
3 update.addBindValue(event.id);
1334
2/3
✓ Branch 48 → 49 taken 3 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 3 times.
3 if (!update.exec()) return false;
1335 3 ++filled;
1336 5 }
1337
7/7
✓ Branch 65 → 66 taken 293 times.
✓ Branch 69 → 70 taken 293 times.
✓ Branch 70 → 71 taken 293 times.
✓ Branch 71 → 72 taken 293 times.
✓ Branch 72 → 73 taken 293 times.
✓ Branch 76 → 67 taken 293 times.
✓ Branch 76 → 77 taken 293 times.
586 qCInfo(lcCalStore) << "series_end filled for" << filled << "recurring rows";
1338 293 return true;
1339 293 }
1340
1341 QList<CalendarEvent>
1342 4784 CalendarStore::eventsForDateRange(const QDateTime &from,
1343 const QDateTime &to) const {
1344 4784 QList<CalendarEvent> result;
1345
1/1
✓ Branch 2 → 3 taken 4784 times.
4784 QSqlQuery q(m_db);
1346 // RDATE rows qualify like recurring masters: an extra date may lie
1347 // after the first occurrence's end. Three disjoint parts so that each one
1348 // uses an index (an OR over them scans the whole table): single events by
1349 // their end, single events without an end, recurring masters. The unary
1350 // "+" keeps SQLite from scanning idx_events_dates for all past events.
1351
4/4
✓ Branch 3 → 4 taken 4784 times.
✓ Branch 4 → 5 taken 4784 times.
✓ Branch 8 → 9 taken 4784 times.
✓ Branch 9 → 10 taken 4784 times.
9568 const QString select = QStringLiteral("SELECT ") + eventColumns(false) + eventFrom(false);
1352 4784 const QString single = QStringLiteral("COALESCE(e.rrule, '') = '' AND COALESCE(e.rdates, '') = ''");
1353
3/3
✓ Branch 34 → 35 taken 4784 times.
✓ Branch 35 → 36 taken 4784 times.
✓ Branch 43 → 44 taken 4784 times.
14352 q.prepare(select + QStringLiteral("WHERE e.dt_end >= ? AND +e.dt_start <= ? AND ") + single +
1354
1/1
✓ Branch 36 → 37 taken 4784 times.
19136 QStringLiteral(" UNION ALL ") +
1355
3/3
✓ Branch 37 → 38 taken 4784 times.
✓ Branch 38 → 39 taken 4784 times.
✓ Branch 39 → 40 taken 4784 times.
19136 select + QStringLiteral("WHERE e.dt_end IS NULL AND e.dt_start <= ? AND ") + single +
1356
1/1
✓ Branch 40 → 41 taken 4784 times.
19136 QStringLiteral(" UNION ALL ") +
1357
2/2
✓ Branch 41 → 42 taken 4784 times.
✓ Branch 42 → 43 taken 4784 times.
19136 select + QStringLiteral("WHERE (COALESCE(e.rrule, '') != '' OR COALESCE(e.rdates, '') != '') AND e.dt_start <= ? "
1358 "AND (e.series_end IS NULL OR e.series_end >= ?)"));
1359
2/2
✓ Branch 64 → 65 taken 4784 times.
✓ Branch 66 → 67 taken 4784 times.
4784 q.addBindValue(from.toSecsSinceEpoch());
1360
2/2
✓ Branch 69 → 70 taken 4784 times.
✓ Branch 71 → 72 taken 4784 times.
4784 q.addBindValue(to.toSecsSinceEpoch());
1361
2/2
✓ Branch 74 → 75 taken 4784 times.
✓ Branch 76 → 77 taken 4784 times.
4784 q.addBindValue(to.toSecsSinceEpoch());
1362
2/2
✓ Branch 79 → 80 taken 4784 times.
✓ Branch 81 → 82 taken 4784 times.
4784 q.addBindValue(to.toSecsSinceEpoch());
1363
2/2
✓ Branch 84 → 85 taken 4784 times.
✓ Branch 86 → 87 taken 4784 times.
4784 q.addBindValue(from.toSecsSinceEpoch());
1364
3/3
✓ Branch 88 → 89 taken 4784 times.
✓ Branch 89 → 90 taken 2 times.
✓ Branch 89 → 106 taken 4782 times.
4784 if (!q.exec()) {
1365
8/8
✓ Branch 90 → 91 taken 2 times.
✓ Branch 94 → 95 taken 2 times.
✓ Branch 95 → 96 taken 2 times.
✓ Branch 96 → 97 taken 2 times.
✓ Branch 97 → 98 taken 2 times.
✓ Branch 98 → 99 taken 2 times.
✓ Branch 104 → 92 taken 2 times.
✓ Branch 104 → 105 taken 2 times.
4 qCWarning(lcCalStore) << "eventsForDateRange failed:" << q.lastError().text();
1366 2 return result;
1367 }
1368
3/3
✓ Branch 111 → 112 taken 11143 times.
✓ Branch 112 → 107 taken 6361 times.
✓ Branch 112 → 113 taken 4782 times.
11143 while (q.next())
1369
2/2
✓ Branch 107 → 108 taken 6361 times.
✓ Branch 108 → 109 taken 6361 times.
6361 result.append(eventFromRow(q, false));
1370 // Sorted here: an ORDER BY over the UNION makes SQLite read the first part
1371 // through the start index again (all past events).
1372
3/3
✓ Branch 113 → 114 taken 4782 times.
✓ Branch 114 → 115 taken 4782 times.
✓ Branch 115 → 116 taken 4782 times.
4782 std::stable_sort(result.begin(), result.end(), [](const CalendarEvent &a, const CalendarEvent &b) {
1373 11417 return a.dtStart < b.dtStart;
1374 });
1375 4782 return result;
1376 4784 }
1377
1378 23 QSet<qint64> CalendarStore::transparentEventIds(const QDateTime &from, const QDateTime &to) const {
1379 23 QSet<qint64> ids;
1380
1/1
✓ Branch 3 → 4 taken 23 times.
23 QSqlQuery q(m_db);
1381 // The same rows as eventsForDateRange: single events overlapping the
1382 // range, and recurring masters still running in it.
1383
1/1
✓ Branch 7 → 8 taken 23 times.
23 q.prepare(QStringLiteral("SELECT id FROM events WHERE instr(ical_raw, 'TRANSP:TRANSPARENT') > 0 AND dt_start <= ? "
1384 "AND (dt_end >= ? OR dt_end IS NULL OR COALESCE(rrule, '') != '' OR COALESCE(rdates, '') != '')"));
1385
2/2
✓ Branch 11 → 12 taken 23 times.
✓ Branch 13 → 14 taken 23 times.
23 q.addBindValue(to.toSecsSinceEpoch());
1386
2/2
✓ Branch 16 → 17 taken 23 times.
✓ Branch 18 → 19 taken 23 times.
23 q.addBindValue(from.toSecsSinceEpoch());
1387
2/3
✓ Branch 20 → 21 taken 23 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 38 taken 23 times.
23 if (!q.exec()) {
1388 ✗ qCWarning(lcCalStore) << "transparentEventIds failed:" << q.lastError().text();
1389 ✗ return ids;
1390 }
1391
6/6
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 44 → 45 taken 24 times.
✓ Branch 45 → 39 taken 1 time.
✓ Branch 45 → 46 taken 23 times.
24 while (q.next()) ids.insert(q.value(0).toLongLong());
1392 23 return ids;
1393 23 }
1394
1395 643 std::optional<CalendarEvent> CalendarStore::eventFor(const QString &uid,
1396 const QString &accountId,
1397 const QString &calendarPath) const {
1398
1/1
✓ Branch 3 → 4 taken 643 times.
643 return occurrenceEvent(uid, accountId, calendarPath, {});
1399 }
1400
1401 932 std::optional<CalendarEvent> CalendarStore::occurrenceEvent(const QString &uid,
1402 const QString &accountId,
1403 const QString &calendarPath,
1404 const QDateTime &recurrenceId) const {
1405
1/1
✓ Branch 2 → 3 taken 932 times.
932 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1406
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 927 times.
932 if (calId < 0)
1407 5 return std::nullopt;
1408
1/1
✓ Branch 5 → 6 taken 927 times.
927 QSqlQuery q(m_db);
1409
5/5
✓ Branch 9 → 10 taken 927 times.
✓ Branch 10 → 11 taken 927 times.
✓ Branch 14 → 15 taken 927 times.
✓ Branch 15 → 16 taken 927 times.
✓ Branch 17 → 18 taken 927 times.
2781 q.prepare(QStringLiteral("SELECT ") + eventColumns(true) + eventFrom(true) +
1410
1/1
✓ Branch 16 → 17 taken 927 times.
2781 QStringLiteral("WHERE e.calendar_id = ? AND e.uid = ? AND e.recurrence_id = ?"));
1411
1/1
✓ Branch 29 → 30 taken 927 times.
927 q.addBindValue(calId);
1412
1/1
✓ Branch 33 → 34 taken 927 times.
927 q.addBindValue(uid);
1413
6/8
✓ Branch 36 → 37 taken 927 times.
✓ Branch 37 → 38 taken 8 times.
✓ Branch 37 → 40 taken 919 times.
✓ Branch 38 → 39 taken 8 times.
✓ Branch 39 → 43 taken 8 times.
✓ Branch 44 → 45 taken 927 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
1854 q.addBindValue(recurrenceId.isValid() ? recurrenceId.toUTC().toString(Qt::ISODate)
1414
4/6
✓ Branch 47 → 48 taken 919 times.
✓ Branch 47 → 49 taken 8 times.
✓ Branch 49 → 50 taken 8 times.
✓ Branch 49 → 51 taken 919 times.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
927 : QStringLiteral(""));
1415
7/8
✓ Branch 51 → 52 taken 927 times.
✓ Branch 52 → 53 taken 927 times.
✗ Branch 52 → 55 not taken.
✓ Branch 53 → 54 taken 927 times.
✓ Branch 54 → 55 taken 98 times.
✓ Branch 54 → 56 taken 829 times.
✓ Branch 57 → 58 taken 98 times.
✓ Branch 57 → 59 taken 829 times.
927 if (!q.exec() || !q.next())
1416 98 return std::nullopt;
1417
1/1
✓ Branch 59 → 60 taken 829 times.
829 return eventFromRow(q, true);
1418 927 }
1419
1420 319 CalendarStore::ReminderState CalendarStore::reminderState(const QString &accountId, const QString &path,
1421 const QString &uid, const QString &occurrence) const {
1422 319 ReminderState state;
1423
1/1
✓ Branch 2 → 3 taken 319 times.
319 const qint64 calId = calendarIdForPath(accountId, path);
1424
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 319 times.
319 if (calId < 0) return state;
1425
1/1
✓ Branch 5 → 6 taken 319 times.
319 QSqlQuery q(m_db);
1426
1/1
✓ Branch 9 → 10 taken 319 times.
319 q.prepare(QStringLiteral("SELECT dismissed, snoozed_until, fired_at FROM reminder_state "
1427 "WHERE calendar_id = ? AND uid = ? AND occurrence = ?"));
1428
1/1
✓ Branch 14 → 15 taken 319 times.
319 q.addBindValue(calId);
1429
1/1
✓ Branch 18 → 19 taken 319 times.
319 q.addBindValue(uid);
1430
1/1
✓ Branch 22 → 23 taken 319 times.
319 q.addBindValue(occurrence);
1431
7/8
✓ Branch 24 → 25 taken 319 times.
✓ Branch 25 → 26 taken 319 times.
✗ Branch 25 → 29 not taken.
✓ Branch 26 → 27 taken 319 times.
✓ Branch 27 → 28 taken 120 times.
✓ Branch 27 → 29 taken 199 times.
✓ Branch 30 → 31 taken 120 times.
✓ Branch 30 → 41 taken 199 times.
319 if (q.exec() && q.next()) {
1432
2/2
✓ Branch 31 → 32 taken 120 times.
✓ Branch 32 → 33 taken 120 times.
120 state.dismissed = q.value(0).toBool();
1433
2/2
✓ Branch 34 → 35 taken 120 times.
✓ Branch 35 → 36 taken 120 times.
120 state.snoozedUntil = q.value(1).toLongLong();
1434
2/2
✓ Branch 37 → 38 taken 120 times.
✓ Branch 38 → 39 taken 120 times.
120 state.firedAt = q.value(2).toLongLong();
1435 }
1436 319 return state;
1437 319 }
1438
1439 38 bool CalendarStore::setReminderState(const QString &accountId, const QString &path, const QString &uid,
1440 const QString &occurrence, const ReminderState &state) {
1441
1/1
✓ Branch 2 → 3 taken 38 times.
38 const qint64 calId = calendarIdForPath(accountId, path);
1442
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 38 times.
38 if (calId < 0) return false;
1443
1/1
✓ Branch 5 → 6 taken 38 times.
38 QSqlQuery q(m_db);
1444
1/1
✓ Branch 9 → 10 taken 38 times.
38 q.prepare(QStringLiteral("INSERT INTO reminder_state (calendar_id, uid, occurrence, dismissed, snoozed_until, fired_at) "
1445 "VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(calendar_id, uid, occurrence) DO UPDATE SET "
1446 "dismissed = excluded.dismissed, snoozed_until = excluded.snoozed_until, "
1447 "fired_at = excluded.fired_at"));
1448
1/1
✓ Branch 14 → 15 taken 38 times.
38 q.addBindValue(calId);
1449
1/1
✓ Branch 18 → 19 taken 38 times.
38 q.addBindValue(uid);
1450
1/1
✓ Branch 22 → 23 taken 38 times.
38 q.addBindValue(occurrence);
1451
3/3
✓ Branch 25 → 26 taken 7 times.
✓ Branch 25 → 27 taken 31 times.
✓ Branch 29 → 30 taken 38 times.
38 q.addBindValue(state.dismissed ? 1 : 0);
1452
1/1
✓ Branch 33 → 34 taken 38 times.
38 q.addBindValue(state.snoozedUntil);
1453
1/1
✓ Branch 37 → 38 taken 38 times.
38 q.addBindValue(state.firedAt);
1454
2/3
✓ Branch 39 → 40 taken 38 times.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 57 taken 38 times.
38 if (!q.exec()) {
1455 ✗ qCWarning(lcCalStore) << "setReminderState:" << q.lastError().text();
1456 ✗ return false;
1457 }
1458 38 return true;
1459 38 }
1460
1461 24 QList<CalendarEvent> CalendarStore::searchEvents(const QStringList &words, int limit) const {
1462 24 QList<CalendarEvent> result;
1463
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 24 times.
24 if (words.isEmpty())
1464 ✗ return result;
1465 // Sprint 103 D-21: with:<name> / mit:<name> match the event's people
1466 // (event_people names and addresses), every other word the texts.
1467 24 QStringList textWords, people;
1468
2/2
✓ Branch 36 → 7 taken 27 times.
✓ Branch 36 → 37 taken 24 times.
51 for (const QString &word : words) {
1469 27 const qsizetype colon = word.indexOf(QLatin1Char(':'));
1470
6/8
✓ Branch 11 → 12 taken 9 times.
✓ Branch 11 → 14 taken 18 times.
✓ Branch 12 → 13 taken 9 times.
✓ Branch 13 → 15 taken 9 times.
✓ Branch 15 → 16 taken 9 times.
✓ Branch 15 → 17 taken 18 times.
✗ Branch 206 → 207 not taken.
✗ Branch 206 → 208 not taken.
27 const QString prefix = colon > 0 ? word.left(colon).toLower() : QString();
1471
7/8
✓ Branch 19 → 20 taken 19 times.
✓ Branch 19 → 23 taken 8 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 26 taken 18 times.
✓ Branch 24 → 25 taken 9 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 9 times.
✓ Branch 27 → 32 taken 18 times.
27 if ((prefix == QLatin1String("with") || prefix == QLatin1String("mit")) && colon + 1 < word.size())
1472
2/2
✓ Branch 28 → 29 taken 9 times.
✓ Branch 29 → 30 taken 9 times.
9 people.append(word.mid(colon + 1));
1473 else
1474
1/1
✓ Branch 32 → 33 taken 18 times.
18 textWords.append(word);
1475 27 }
1476 // SQLite's LIKE/lower() fold ASCII only; the texts are matched here so
1477 // "Größe" finds "größe". Only the three text columns (and, for with:, the
1478 // people) are read.
1479
1/1
✓ Branch 37 → 38 taken 24 times.
24 QSqlQuery scan(m_db);
1480 48 const QString peopleColumn = people.isEmpty() ? QStringLiteral("''")
1481
6/6
✓ Branch 39 → 40 taken 15 times.
✓ Branch 39 → 43 taken 9 times.
✓ Branch 46 → 47 taken 9 times.
✓ Branch 46 → 48 taken 15 times.
✓ Branch 48 → 49 taken 15 times.
✓ Branch 48 → 50 taken 9 times.
48 : QStringLiteral("(SELECT group_concat(p.name || ' ' || p.address, char(10)) FROM event_people p"
1482 " WHERE p.event_id = e.id)");
1483
1/1
✓ Branch 58 → 59 taken 24 times.
72 if (!scan.exec(QStringLiteral("SELECT e.calendar_id, e.uid, e.summary, e.location, e.description, ") +
1484
3/4
✓ Branch 56 → 57 taken 24 times.
✓ Branch 57 → 58 taken 24 times.
✗ Branch 65 → 66 not taken.
✓ Branch 65 → 67 taken 24 times.
96 peopleColumn + QStringLiteral(" FROM events e")))
1485 ✗ return result;
1486 24 QList<QPair<qint64, QString>> matches;
1487
6/7
✓ Branch 137 → 138 taken 77 times.
✓ Branch 138 → 139 taken 53 times.
✓ Branch 138 → 142 taken 24 times.
✓ Branch 140 → 141 taken 53 times.
✗ Branch 140 → 142 not taken.
✓ Branch 143 → 68 taken 53 times.
✓ Branch 143 → 144 taken 24 times.
77 while (scan.next() && matches.size() < limit) {
1488
6/6
✓ Branch 72 → 73 taken 53 times.
✓ Branch 73 → 74 taken 53 times.
✓ Branch 76 → 77 taken 53 times.
✓ Branch 77 → 78 taken 53 times.
✓ Branch 78 → 79 taken 53 times.
✓ Branch 79 → 80 taken 53 times.
106 const QString text = scan.value(2).toString() + QLatin1Char('\n') + scan.value(3).toString() +
1489
4/4
✓ Branch 68 → 69 taken 53 times.
✓ Branch 69 → 70 taken 53 times.
✓ Branch 80 → 81 taken 53 times.
✓ Branch 81 → 82 taken 53 times.
159 QLatin1Char('\n') + scan.value(4).toString();
1490
2/2
✓ Branch 91 → 92 taken 53 times.
✓ Branch 92 → 93 taken 53 times.
53 const QString persons = scan.value(5).toString();
1491 53 bool all = true;
1492
8/9
✓ Branch 94 → 95 taken 53 times.
✓ Branch 95 → 96 taken 53 times.
✓ Branch 98 → 99 taken 33 times.
✗ Branch 98 → 102 not taken.
✓ Branch 99 → 100 taken 33 times.
✓ Branch 100 → 101 taken 14 times.
✓ Branch 100 → 102 taken 19 times.
✓ Branch 105 → 97 taken 33 times.
✓ Branch 105 → 106 taken 53 times.
86 for (const QString &word : textWords) all = all && text.contains(word, Qt::CaseInsensitive);
1493
9/9
✓ Branch 106 → 107 taken 53 times.
✓ Branch 107 → 108 taken 53 times.
✓ Branch 110 → 111 taken 24 times.
✓ Branch 110 → 114 taken 7 times.
✓ Branch 111 → 112 taken 24 times.
✓ Branch 112 → 113 taken 8 times.
✓ Branch 112 → 114 taken 16 times.
✓ Branch 117 → 109 taken 31 times.
✓ Branch 117 → 118 taken 53 times.
84 for (const QString &person : people) all = all && persons.contains(person, Qt::CaseInsensitive);
1494
4/4
✓ Branch 118 → 119 taken 53 times.
✓ Branch 119 → 120 taken 53 times.
✓ Branch 120 → 121 taken 53 times.
✓ Branch 121 → 122 taken 53 times.
53 const QPair<qint64, QString> key{scan.value(0).toLongLong(), scan.value(1).toString()};
1495
6/7
✓ Branch 126 → 127 taken 18 times.
✓ Branch 126 → 130 taken 35 times.
✓ Branch 128 → 129 taken 18 times.
✗ Branch 128 → 130 not taken.
✓ Branch 131 → 132 taken 18 times.
✓ Branch 131 → 133 taken 35 times.
✓ Branch 132 → 133 taken 18 times.
53 if (all && !matches.contains(key)) matches.append(key);
1496 53 }
1497
4/4
✓ Branch 144 → 145 taken 24 times.
✓ Branch 145 → 146 taken 24 times.
✓ Branch 198 → 147 taken 18 times.
✓ Branch 198 → 199 taken 24 times.
42 for (const auto &[calendarId, uid] : matches) {
1498
1/1
✓ Branch 150 → 151 taken 18 times.
18 QSqlQuery q(m_db);
1499
5/5
✓ Branch 154 → 155 taken 18 times.
✓ Branch 155 → 156 taken 18 times.
✓ Branch 159 → 160 taken 18 times.
✓ Branch 160 → 161 taken 18 times.
✓ Branch 162 → 163 taken 18 times.
54 q.prepare(QStringLiteral("SELECT ") + eventColumns(false) + eventFrom(false) +
1500
1/1
✓ Branch 161 → 162 taken 18 times.
54 QStringLiteral("WHERE e.calendar_id = ? AND e.uid = ? ORDER BY e.recurrence_id ASC"));
1501
1/1
✓ Branch 174 → 175 taken 18 times.
18 q.addBindValue(calendarId);
1502
1/1
✓ Branch 178 → 179 taken 18 times.
18 q.addBindValue(uid);
1503
2/3
✓ Branch 180 → 181 taken 18 times.
✗ Branch 181 → 182 not taken.
✓ Branch 181 → 183 taken 18 times.
18 if (!q.exec()) continue;
1504
5/5
✓ Branch 184 → 185 taken 18 times.
✓ Branch 185 → 186 taken 18 times.
✓ Branch 188 → 189 taken 36 times.
✓ Branch 189 → 184 taken 18 times.
✓ Branch 189 → 190 taken 18 times.
36 while (q.next()) result.append(eventFromRow(q, false));
1505 18 }
1506 24 return result;
1507 24 }
1508
1509 169 QList<QPair<QString, QString>> CalendarStore::calendarsWithUid(const QString &uid) const {
1510 169 QList<QPair<QString, QString>> result;
1511
1/1
✓ Branch 2 → 3 taken 169 times.
169 QSqlQuery q(m_db);
1512
1/1
✓ Branch 6 → 7 taken 169 times.
169 q.prepare(QStringLiteral("SELECT DISTINCT c.accountId, c.path FROM events e "
1513 "JOIN calendars c ON c.id = e.calendar_id WHERE e.uid = ? ORDER BY c.id"));
1514
1/1
✓ Branch 11 → 12 taken 169 times.
169 q.addBindValue(uid);
1515
2/3
✓ Branch 13 → 14 taken 169 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 31 taken 169 times.
169 if (!q.exec()) {
1516 ✗ qCWarning(lcCalStore) << "calendarsWithUid:" << q.lastError().text();
1517 ✗ return result;
1518 }
1519
3/3
✓ Branch 44 → 45 taken 265 times.
✓ Branch 45 → 32 taken 96 times.
✓ Branch 45 → 46 taken 169 times.
265 while (q.next())
1520
5/5
✓ Branch 32 → 33 taken 96 times.
✓ Branch 33 → 34 taken 96 times.
✓ Branch 34 → 35 taken 96 times.
✓ Branch 35 → 36 taken 96 times.
✓ Branch 37 → 38 taken 96 times.
96 result.append({q.value(0).toString(), q.value(1).toString()});
1521 169 return result;
1522 169 }
1523
1524 654 QList<CalendarEvent> CalendarStore::eventSeries(const QString &uid, const QString &accountId,
1525 const QString &calendarPath) const {
1526 654 QList<CalendarEvent> result;
1527
1/1
✓ Branch 2 → 3 taken 654 times.
654 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1528
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 653 times.
654 if (calId < 0)
1529 1 return result;
1530
1/1
✓ Branch 5 → 6 taken 653 times.
653 QSqlQuery q(m_db);
1531
5/5
✓ Branch 9 → 10 taken 653 times.
✓ Branch 10 → 11 taken 653 times.
✓ Branch 14 → 15 taken 653 times.
✓ Branch 15 → 16 taken 653 times.
✓ Branch 17 → 18 taken 653 times.
1959 q.prepare(QStringLiteral("SELECT ") + eventColumns(true) + eventFrom(true) +
1532
1/1
✓ Branch 16 → 17 taken 653 times.
1959 QStringLiteral("WHERE e.calendar_id = ? AND e.uid = ? ORDER BY e.recurrence_id ASC"));
1533
1/1
✓ Branch 29 → 30 taken 653 times.
653 q.addBindValue(calId);
1534
1/1
✓ Branch 33 → 34 taken 653 times.
653 q.addBindValue(uid);
1535
2/3
✓ Branch 35 → 36 taken 653 times.
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 653 times.
653 if (!q.exec())
1536 ✗ return result;
1537
3/3
✓ Branch 43 → 44 taken 1284 times.
✓ Branch 44 → 39 taken 631 times.
✓ Branch 44 → 45 taken 653 times.
1284 while (q.next())
1538
2/2
✓ Branch 39 → 40 taken 631 times.
✓ Branch 40 → 41 taken 631 times.
631 result.append(eventFromRow(q, true));
1539 653 return result;
1540 653 }
1541
1542 QList<CalendarEvent>
1543 9 CalendarStore::eventsForCalendar(const QString &calendarPath) const {
1544
1/1
✓ Branch 3 → 4 taken 9 times.
9 return eventsForCalendar(QString(), calendarPath);
1545 }
1546
1547 QList<CalendarEvent>
1548 128 CalendarStore::eventsForCalendar(const QString &accountId,
1549 const QString &calendarPath) const {
1550 128 QList<CalendarEvent> result;
1551
1/1
✓ Branch 2 → 3 taken 128 times.
128 qint64 calId = calendarIdForPath(accountId, calendarPath);
1552
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 122 times.
128 if (calId < 0)
1553 6 return result;
1554
1555
1/1
✓ Branch 5 → 6 taken 122 times.
122 QSqlQuery q(m_db);
1556
5/5
✓ Branch 9 → 10 taken 122 times.
✓ Branch 10 → 11 taken 122 times.
✓ Branch 14 → 15 taken 122 times.
✓ Branch 15 → 16 taken 122 times.
✓ Branch 17 → 18 taken 122 times.
366 q.prepare(QStringLiteral("SELECT ") + eventColumns(true) + eventFrom(true) +
1557
1/1
✓ Branch 16 → 17 taken 122 times.
366 QStringLiteral("WHERE e.calendar_id = ? ORDER BY e.dt_start ASC"));
1558
1/1
✓ Branch 29 → 30 taken 122 times.
122 q.addBindValue(calId);
1559
3/3
✓ Branch 31 → 32 taken 122 times.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 121 times.
122 if (!q.exec())
1560 1 return result;
1561
1562
3/3
✓ Branch 45 → 46 taken 561 times.
✓ Branch 46 → 35 taken 440 times.
✓ Branch 46 → 47 taken 121 times.
561 while (q.next()) {
1563
1/1
✓ Branch 35 → 36 taken 440 times.
440 CalendarEvent ev = eventFromRow(q, true);
1564 // Callers address the calendar by the identity they passed in.
1565 440 ev.calendarPath = calendarPath;
1566
2/2
✓ Branch 38 → 39 taken 7 times.
✓ Branch 38 → 40 taken 433 times.
440 ev.accountId = accountId.isEmpty() ? ev.accountId : accountId;
1567
1/1
✓ Branch 42 → 43 taken 440 times.
440 result.append(ev);
1568 440 }
1569 121 return result;
1570 122 }
1571
1572 1 bool CalendarStore::removeStaleEvents(const QString &calendarPath,
1573 const QStringList &activeUids,
1574 bool allowEmpty) {
1575
1/1
✓ Branch 3 → 4 taken 1 time.
1 return removeStaleEvents(QString(), calendarPath, activeUids, allowEmpty);
1576 }
1577
1578 14 bool CalendarStore::removeStaleEvents(const QString &accountId,
1579 const QString &calendarPath,
1580 const QStringList &activeUids,
1581 bool allowEmpty) {
1582
1/1
✓ Branch 2 → 3 taken 14 times.
14 qint64 calId = calendarIdForPath(accountId, calendarPath);
1583
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 13 times.
14 if (calId < 0)
1584 1 return false;
1585
1586
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 54 taken 10 times.
13 if (activeUids.isEmpty()) {
1587 // SEC-2026-07-21-17: Guard against empty list — a valid-but-empty sync
1588 // response (e.g. from a transparent proxy or broken server upgrade) must
1589 // not silently delete the entire local cache including locally created,
1590 // not-yet-uploaded entries.
1591
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 20 taken 2 times.
3 if (!allowEmpty) {
1592 2 qCWarning(lcCalStore) // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; migration failure remains measured.
1593
1/1
✓ Branch 13 → 14 taken 1 time.
1 << "removeStaleEvents: activeUids is empty — skipping delete"
1594
1/1
✓ Branch 14 → 15 taken 1 time.
1 << "(use allowEmpty=true to force)";
1595 1 return true;
1596 }
1597
1/1
✓ Branch 20 → 21 taken 2 times.
2 QSqlQuery q(m_db);
1598
1/1
✓ Branch 24 → 25 taken 2 times.
2 q.prepare(QStringLiteral("DELETE FROM events WHERE calendar_id = ?"));
1599
1/1
✓ Branch 29 → 30 taken 2 times.
2 q.addBindValue(calId);
1600
1/1
✓ Branch 31 → 32 taken 2 times.
2 QSqlQuery resources(m_db);
1601
1/1
✓ Branch 35 → 36 taken 2 times.
2 resources.prepare(QStringLiteral("DELETE FROM calendar_resources WHERE calendar_id = ?"));
1602
1/1
✓ Branch 40 → 41 taken 2 times.
2 resources.addBindValue(calId);
1603
6/9
✓ Branch 42 → 43 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
✗ Branch 43 → 49 not taken.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 45 → 46 taken 2 times.
✗ Branch 45 → 49 not taken.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 47 → 48 taken 2 times.
✗ Branch 47 → 49 not taken.
2 return q.exec() && resources.exec() && pruneReminderState(calId);
1604 2 }
1605
1606 10 QString error;
1607 10 bool ok = true;
1608
4/6
✓ Branch 64 → 65 taken 10 times.
✓ Branch 64 → 66 taken 10 times.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 79 taken 9 times.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
20 if (!DatabaseSync::removeRowsMissingFromUidSet(
1609
1/1
✓ Branch 59 → 60 taken 10 times.
10 m_db,
1610 20 QStringLiteral(
1611 "DELETE FROM events WHERE calendar_id = ? "
1612 "AND NOT EXISTS (SELECT 1 FROM mailjd_active_sync_uids active "
1613 "WHERE active.uid = events.uid)"),
1614 {calId}, activeUids, &error)) {
1615
6/6
✓ Branch 67 → 68 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 72 → 73 taken 1 time.
✓ Branch 73 → 74 taken 1 time.
✓ Branch 77 → 69 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
2 qCWarning(lcCalStore) << "removeStaleEvents:" << error;
1616 1 ok = false;
1617 }
1618
4/6
✓ Branch 89 → 90 taken 10 times.
✓ Branch 89 → 91 taken 10 times.
✓ Branch 91 → 92 taken 1 time.
✓ Branch 91 → 104 taken 9 times.
✗ Branch 176 → 177 not taken.
✗ Branch 176 → 178 not taken.
20 if (!DatabaseSync::removeRowsMissingFromUidSet(
1619
1/1
✓ Branch 84 → 85 taken 10 times.
10 m_db,
1620 20 QStringLiteral(
1621 "DELETE FROM calendar_resources WHERE calendar_id = ? "
1622 "AND NOT EXISTS (SELECT 1 FROM mailjd_active_sync_uids active "
1623 "WHERE active.uid = calendar_resources.uid) "
1624 "AND NOT EXISTS (SELECT 1 FROM tasks WHERE tasks.calendar_id = calendar_resources.calendar_id AND tasks.uid = calendar_resources.uid)"),
1625 {calId}, activeUids, &error)) {
1626
6/6
✓ Branch 92 → 93 taken 1 time.
✓ Branch 96 → 97 taken 1 time.
✓ Branch 97 → 98 taken 1 time.
✓ Branch 98 → 99 taken 1 time.
✓ Branch 102 → 94 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
2 qCWarning(lcCalStore) << "removeStaleEvents (resources):" << error;
1627 1 ok = false;
1628 }
1629
4/5
✓ Branch 104 → 105 taken 10 times.
✓ Branch 105 → 106 taken 9 times.
✓ Branch 105 → 108 taken 1 time.
✓ Branch 106 → 107 taken 9 times.
✗ Branch 106 → 108 not taken.
10 return pruneReminderState(calId) && ok;
1630 30 }
1631
1632 5 bool CalendarStore::deleteEvent(const QString &uid,
1633 const QString &calendarPath) {
1634
1/1
✓ Branch 4 → 5 taken 5 times.
5 return deleteEvent(uid, QString(), calendarPath);
1635 }
1636
1637 663 bool CalendarStore::deleteEvent(const QString &uid,
1638 const QString &accountId,
1639 const QString &calendarPath,
1640 const QDateTime &recurrenceId) {
1641
1/1
✓ Branch 2 → 3 taken 663 times.
663 qint64 calId = calendarIdForPath(accountId, calendarPath);
1642
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 16 taken 660 times.
663 if (calId < 0) {
1643
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(lcCalStore) << "deleteEvent: calendar not found:" << calendarPath;
1644 3 return false;
1645 }
1646
1647
1/1
✓ Branch 16 → 17 taken 660 times.
660 QSqlQuery q(m_db);
1648
3/3
✓ Branch 17 → 18 taken 660 times.
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 42 taken 657 times.
660 if (recurrenceId.isValid()) {
1649 // Delete exactly one exception of the series.
1650
1/1
✓ Branch 22 → 23 taken 3 times.
3 q.prepare(QStringLiteral(
1651 "DELETE FROM events WHERE uid = ? AND calendar_id = ? AND recurrence_id = ?"));
1652
1/1
✓ Branch 27 → 28 taken 3 times.
3 q.addBindValue(uid);
1653
1/1
✓ Branch 31 → 32 taken 3 times.
3 q.addBindValue(calId);
1654
3/3
✓ Branch 34 → 35 taken 3 times.
✓ Branch 35 → 36 taken 3 times.
✓ Branch 37 → 38 taken 3 times.
3 q.addBindValue(recurrenceId.toUTC().toString(Qt::ISODate));
1655 } else {
1656 // Master deletion cascades over its exceptions (same uid).
1657
1/1
✓ Branch 45 → 46 taken 657 times.
657 q.prepare(
1658 1314 QStringLiteral("DELETE FROM events WHERE uid = ? AND calendar_id = ?"));
1659
1/1
✓ Branch 50 → 51 taken 657 times.
657 q.addBindValue(uid);
1660
1/1
✓ Branch 54 → 55 taken 657 times.
657 q.addBindValue(calId);
1661 }
1662
3/3
✓ Branch 57 → 58 taken 660 times.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 75 taken 659 times.
660 if (!q.exec()) {
1663
8/8
✓ Branch 59 → 60 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 73 → 61 taken 1 time.
✓ Branch 73 → 74 taken 1 time.
2 qCWarning(lcCalStore) << "deleteEvent:" << q.lastError().text();
1664 1 return false;
1665 }
1666 // The resource row stays: it is the merge base of a pending delete and is
1667 // removed by removeResource() once the server confirmed the deletion.
1668
1/1
✓ Branch 75 → 76 taken 659 times.
659 return q.numRowsAffected() > 0;
1669 660 }
1670
1671 34 bool CalendarStore::removeResource(const QString &accountId, const QString &calendarPath,
1672 const QString &uid) {
1673
1/1
✓ Branch 2 → 3 taken 34 times.
34 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1674
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 34 times.
34 if (calId < 0)
1675 ✗ return false;
1676
1/1
✓ Branch 5 → 6 taken 34 times.
34 QSqlQuery q(m_db);
1677
1/1
✓ Branch 9 → 10 taken 34 times.
34 q.prepare(QStringLiteral("DELETE FROM calendar_resources WHERE calendar_id = ? AND uid = ?"));
1678
1/1
✓ Branch 14 → 15 taken 34 times.
34 q.addBindValue(calId);
1679
1/1
✓ Branch 18 → 19 taken 34 times.
34 q.addBindValue(uid);
1680
4/6
✓ Branch 20 → 21 taken 34 times.
✓ Branch 21 → 22 taken 34 times.
✗ Branch 21 → 25 not taken.
✓ Branch 22 → 23 taken 34 times.
✓ Branch 23 → 24 taken 34 times.
✗ Branch 23 → 25 not taken.
34 return q.exec() && pruneReminderState(calId);
1681 34 }
1682
1683 // S106-140: the reminder state (dismissed, snoozed, fired) of events that
1684 // no longer exist. It is pruned where an event really goes — its resource
1685 // removed (deletion confirmed, or local only) or gone from the server — and
1686 // never on the delete-and-reinsert of a sync update, which keeps it.
1687 46 bool CalendarStore::pruneReminderState(qint64 calendarId) {
1688
1/1
✓ Branch 2 → 3 taken 46 times.
46 QSqlQuery q(m_db);
1689
1/1
✓ Branch 6 → 7 taken 46 times.
46 q.prepare(QStringLiteral(
1690 "DELETE FROM reminder_state WHERE calendar_id = ? AND NOT EXISTS (SELECT 1 FROM events e "
1691 "WHERE e.calendar_id = reminder_state.calendar_id AND e.uid = reminder_state.uid)"));
1692
1/1
✓ Branch 11 → 12 taken 46 times.
46 q.addBindValue(calendarId);
1693
3/3
✓ Branch 13 → 14 taken 46 times.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 31 taken 45 times.
46 if (!q.exec()) {
1694
8/8
✓ Branch 15 → 16 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 29 → 17 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
2 qCWarning(lcCalStore) << "pruneReminderState:" << q.lastError().text();
1695 1 return false;
1696 }
1697 45 return true;
1698 46 }
1699
1700 // ═══════════════════════════════════════════════════════
1701 // Write outbox (Sprint 93 S93.1.6)
1702 // ═══════════════════════════════════════════════════════
1703
1704 static const char *kOutboxSelect =
1705 "SELECT c.accountId, c.path, o.uid, o.operation, o.href, o.base_etag, o.revision, "
1706 " o.attempts, o.next_attempt, o.last_error, o.state, o.conflict_fields, "
1707 " o.server_ical, o.server_etag, o.created, o.component_type, o.dependency_key "
1708 "FROM calendar_outbox o JOIN calendars c ON o.calendar_id = c.id ";
1709
1710 2418 static CalendarOutboxEntry outboxFromRow(const QSqlQuery &q) {
1711 2418 CalendarOutboxEntry entry;
1712
2/2
✓ Branch 3 → 4 taken 2418 times.
✓ Branch 4 → 5 taken 2418 times.
2418 entry.accountId = q.value(0).toString();
1713
2/2
✓ Branch 8 → 9 taken 2418 times.
✓ Branch 9 → 10 taken 2418 times.
2418 entry.calendarPath = q.value(1).toString();
1714
2/2
✓ Branch 13 → 14 taken 2418 times.
✓ Branch 14 → 15 taken 2418 times.
2418 entry.uid = q.value(2).toString();
1715
2/2
✓ Branch 18 → 19 taken 2418 times.
✓ Branch 19 → 20 taken 2418 times.
2418 entry.operation = q.value(3).toString();
1716
2/2
✓ Branch 23 → 24 taken 2418 times.
✓ Branch 24 → 25 taken 2418 times.
2418 entry.href = q.value(4).toString();
1717
2/2
✓ Branch 28 → 29 taken 2418 times.
✓ Branch 29 → 30 taken 2418 times.
2418 entry.baseEtag = q.value(5).toString();
1718
2/2
✓ Branch 33 → 34 taken 2418 times.
✓ Branch 34 → 35 taken 2418 times.
2418 entry.revision = q.value(6).toInt();
1719
2/2
✓ Branch 36 → 37 taken 2418 times.
✓ Branch 37 → 38 taken 2418 times.
2418 entry.attempts = q.value(7).toInt();
1720
2/2
✓ Branch 39 → 40 taken 2418 times.
✓ Branch 40 → 41 taken 2418 times.
2418 entry.nextAttempt = q.value(8).toLongLong();
1721
2/2
✓ Branch 42 → 43 taken 2418 times.
✓ Branch 43 → 44 taken 2418 times.
2418 entry.lastError = q.value(9).toString();
1722
2/2
✓ Branch 47 → 48 taken 2418 times.
✓ Branch 48 → 49 taken 2418 times.
2418 entry.state = q.value(10).toString();
1723
3/3
✓ Branch 52 → 53 taken 2418 times.
✓ Branch 53 → 54 taken 2418 times.
✓ Branch 57 → 58 taken 2418 times.
2418 entry.conflictFields = q.value(11).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
1724
2/2
✓ Branch 62 → 63 taken 2418 times.
✓ Branch 63 → 64 taken 2418 times.
2418 entry.serverIcal = q.value(12).toString();
1725
2/2
✓ Branch 67 → 68 taken 2418 times.
✓ Branch 68 → 69 taken 2418 times.
2418 entry.serverEtag = q.value(13).toString();
1726
2/2
✓ Branch 72 → 73 taken 2418 times.
✓ Branch 73 → 74 taken 2418 times.
2418 entry.created = q.value(14).toLongLong();
1727
2/2
✓ Branch 75 → 76 taken 2418 times.
✓ Branch 76 → 77 taken 2418 times.
2418 entry.componentType = q.value(15).toString();
1728
2/2
✓ Branch 80 → 81 taken 2418 times.
✓ Branch 81 → 82 taken 2418 times.
2418 entry.dependencyKey = q.value(16).toString();
1729 2418 return entry;
1730 ✗ }
1731
1732 374 int CalendarStore::enqueueOutbox(const QString &accountId, const QString &calendarPath,
1733 const QString &uid, const QString &operation,
1734 const QString &href, const QString &baseEtag) {
1735
1/1
✓ Branch 2 → 3 taken 374 times.
374 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1736
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 374 times.
374 if (calId < 0)
1737 ✗ return 0;
1738 // Coalescing: the first entry keeps the server base (href, etag) the
1739 // local change started from; later changes only bump the revision.
1740 // S106-130: a new change also re-opens an entry that ended 'failed' or
1741 // 'conflict' — it is pushed again (a conflict is re-evaluated against the
1742 // server with the newest edit), and a stale server copy is dropped so a
1743 // later "keep the server version" cannot overwrite the newer edit.
1744
1/1
✓ Branch 5 → 6 taken 374 times.
374 QSqlQuery q(m_db);
1745
1/1
✓ Branch 9 → 10 taken 374 times.
374 q.prepare(QStringLiteral(
1746 "INSERT INTO calendar_outbox (calendar_id, uid, operation, href, base_etag, revision, "
1747 " attempts, next_attempt, state, created) VALUES (?, ?, ?, ?, ?, 1, 0, 0, 'pending', ?) "
1748 "ON CONFLICT(calendar_id, uid) DO UPDATE SET operation = excluded.operation, "
1749 " revision = calendar_outbox.revision + 1, attempts = 0, next_attempt = 0, "
1750 " state = 'pending', last_error = NULL, conflict_fields = NULL, server_ical = NULL, "
1751 " server_etag = NULL, "
1752 " href = CASE WHEN COALESCE(calendar_outbox.href, '') = '' THEN excluded.href "
1753 " ELSE calendar_outbox.href END"));
1754
1/1
✓ Branch 14 → 15 taken 374 times.
374 q.addBindValue(calId);
1755
1/1
✓ Branch 18 → 19 taken 374 times.
374 q.addBindValue(uid);
1756
1/1
✓ Branch 22 → 23 taken 374 times.
374 q.addBindValue(operation);
1757
1/1
✓ Branch 26 → 27 taken 374 times.
374 q.addBindValue(href);
1758
1/1
✓ Branch 30 → 31 taken 374 times.
374 q.addBindValue(baseEtag);
1759
1/1
✓ Branch 35 → 36 taken 374 times.
374 q.addBindValue(QDateTime::currentSecsSinceEpoch());
1760
2/3
✓ Branch 37 → 38 taken 374 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 55 taken 374 times.
374 if (!q.exec()) {
1761 ✗ qCWarning(lcCalStore) << "enqueueOutbox:" << q.lastError().text();
1762 ✗ return 0;
1763 }
1764
1/1
✓ Branch 55 → 56 taken 374 times.
374 const auto entry = outboxEntry(accountId, calendarPath, uid);
1765
1/2
✓ Branch 57 → 58 taken 374 times.
✗ Branch 57 → 60 not taken.
374 return entry ? entry->revision : 0;
1766 374 }
1767
1768 1550 QList<CalendarOutboxEntry> CalendarStore::outboxEntries() const {
1769 1550 QList<CalendarOutboxEntry> entries;
1770
1/1
✓ Branch 2 → 3 taken 1550 times.
1550 QSqlQuery q(m_db);
1771
4/5
✓ Branch 7 → 8 taken 1550 times.
✓ Branch 8 → 9 taken 1550 times.
✓ Branch 9 → 10 taken 1550 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1550 times.
1550 if (!q.exec(QString::fromLatin1(kOutboxSelect) + QStringLiteral("ORDER BY o.created ASC")))
1772 ✗ return entries;
1773
3/3
✓ Branch 21 → 22 taken 2370 times.
✓ Branch 22 → 17 taken 820 times.
✓ Branch 22 → 23 taken 1550 times.
2370 while (q.next())
1774
2/2
✓ Branch 17 → 18 taken 820 times.
✓ Branch 18 → 19 taken 820 times.
820 entries.append(outboxFromRow(q));
1775 1550 return entries;
1776 1550 }
1777
1778 1872 std::optional<CalendarOutboxEntry> CalendarStore::outboxEntry(const QString &accountId,
1779 const QString &calendarPath,
1780 const QString &uid) const {
1781
1/1
✓ Branch 2 → 3 taken 1872 times.
1872 QSqlQuery q(m_db);
1782
2/2
✓ Branch 7 → 8 taken 1872 times.
✓ Branch 9 → 10 taken 1872 times.
3744 q.prepare(QString::fromLatin1(kOutboxSelect) +
1783
1/1
✓ Branch 8 → 9 taken 1872 times.
5616 QStringLiteral("WHERE c.accountId = ? AND c.path = ? AND o.uid = ?"));
1784
1/1
✓ Branch 16 → 17 taken 1872 times.
1872 q.addBindValue(accountId);
1785
1/1
✓ Branch 20 → 21 taken 1872 times.
1872 q.addBindValue(calendarPath);
1786
1/1
✓ Branch 24 → 25 taken 1872 times.
1872 q.addBindValue(uid);
1787
7/8
✓ Branch 26 → 27 taken 1872 times.
✓ Branch 27 → 28 taken 1872 times.
✗ Branch 27 → 30 not taken.
✓ Branch 28 → 29 taken 1872 times.
✓ Branch 29 → 30 taken 274 times.
✓ Branch 29 → 31 taken 1598 times.
✓ Branch 32 → 33 taken 274 times.
✓ Branch 32 → 34 taken 1598 times.
1872 if (!q.exec() || !q.next())
1788 274 return std::nullopt;
1789
1/1
✓ Branch 34 → 35 taken 1598 times.
1598 return outboxFromRow(q);
1790 1872 }
1791
1792 300 bool CalendarStore::updateOutbox(const CalendarOutboxEntry &entry) {
1793
1/1
✓ Branch 2 → 3 taken 300 times.
300 const qint64 calId = calendarIdForPath(entry.accountId, entry.calendarPath);
1794
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 300 times.
300 if (calId < 0)
1795 ✗ return false;
1796
1/1
✓ Branch 5 → 6 taken 300 times.
300 QSqlQuery q(m_db);
1797
1/1
✓ Branch 9 → 10 taken 300 times.
300 q.prepare(QStringLiteral(
1798 "UPDATE calendar_outbox SET operation = ?, href = ?, base_etag = ?, attempts = ?, "
1799 " next_attempt = ?, last_error = ?, state = ?, conflict_fields = ?, server_ical = ?, "
1800 " server_etag = ?, component_type = ?, dependency_key = ? WHERE calendar_id = ? AND uid = ?"));
1801
1/1
✓ Branch 14 → 15 taken 300 times.
300 q.addBindValue(entry.operation);
1802
1/1
✓ Branch 18 → 19 taken 300 times.
300 q.addBindValue(entry.href);
1803
1/1
✓ Branch 22 → 23 taken 300 times.
300 q.addBindValue(entry.baseEtag);
1804
1/1
✓ Branch 26 → 27 taken 300 times.
300 q.addBindValue(entry.attempts);
1805
1/1
✓ Branch 30 → 31 taken 300 times.
300 q.addBindValue(entry.nextAttempt);
1806
1/1
✓ Branch 34 → 35 taken 300 times.
300 q.addBindValue(entry.lastError);
1807
1/1
✓ Branch 38 → 39 taken 300 times.
300 q.addBindValue(entry.state);
1808
2/2
✓ Branch 43 → 44 taken 300 times.
✓ Branch 45 → 46 taken 300 times.
300 q.addBindValue(entry.conflictFields.join(QLatin1Char(',')));
1809
1/1
✓ Branch 50 → 51 taken 300 times.
300 q.addBindValue(entry.serverIcal);
1810
1/1
✓ Branch 54 → 55 taken 300 times.
300 q.addBindValue(entry.serverEtag);
1811
1/1
✓ Branch 58 → 59 taken 300 times.
300 q.addBindValue(entry.componentType);
1812
1/1
✓ Branch 62 → 63 taken 300 times.
300 q.addBindValue(entry.dependencyKey);
1813
1/1
✓ Branch 66 → 67 taken 300 times.
300 q.addBindValue(calId);
1814
1/1
✓ Branch 70 → 71 taken 300 times.
300 q.addBindValue(entry.uid);
1815
2/3
✓ Branch 72 → 73 taken 300 times.
✗ Branch 73 → 74 not taken.
✓ Branch 73 → 90 taken 300 times.
300 if (!q.exec()) {
1816 ✗ qCWarning(lcCalStore) << "updateOutbox:" << q.lastError().text();
1817 ✗ return false;
1818 }
1819
1/1
✓ Branch 90 → 91 taken 300 times.
300 return q.numRowsAffected() > 0;
1820 300 }
1821
1822 140 bool CalendarStore::removeOutbox(const QString &accountId, const QString &calendarPath,
1823 const QString &uid, int revision) {
1824
1/1
✓ Branch 2 → 3 taken 140 times.
140 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1825
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 140 times.
140 if (calId < 0)
1826 ✗ return false;
1827
1/1
✓ Branch 5 → 6 taken 140 times.
140 QSqlQuery q(m_db);
1828
2/2
✓ Branch 6 → 7 taken 62 times.
✓ Branch 6 → 26 taken 78 times.
140 if (revision > 0) {
1829
1/1
✓ Branch 10 → 11 taken 62 times.
62 q.prepare(QStringLiteral(
1830 "DELETE FROM calendar_outbox WHERE calendar_id = ? AND uid = ? AND revision = ?"));
1831
1/1
✓ Branch 15 → 16 taken 62 times.
62 q.addBindValue(calId);
1832
1/1
✓ Branch 19 → 20 taken 62 times.
62 q.addBindValue(uid);
1833
1/1
✓ Branch 23 → 24 taken 62 times.
62 q.addBindValue(revision);
1834 } else {
1835
1/1
✓ Branch 29 → 30 taken 78 times.
78 q.prepare(QStringLiteral("DELETE FROM calendar_outbox WHERE calendar_id = ? AND uid = ?"));
1836
1/1
✓ Branch 34 → 35 taken 78 times.
78 q.addBindValue(calId);
1837
1/1
✓ Branch 38 → 39 taken 78 times.
78 q.addBindValue(uid);
1838 }
1839
5/6
✓ Branch 41 → 42 taken 140 times.
✓ Branch 42 → 43 taken 140 times.
✗ Branch 42 → 46 not taken.
✓ Branch 43 → 44 taken 140 times.
✓ Branch 44 → 45 taken 126 times.
✓ Branch 44 → 46 taken 14 times.
140 return q.exec() && q.numRowsAffected() > 0;
1840 140 }
1841
1842 177 QStringList CalendarStore::outboxUids(const QString &accountId, const QString &calendarPath) const {
1843 177 QStringList uids;
1844
1/1
✓ Branch 2 → 3 taken 177 times.
177 const qint64 calId = calendarIdForPath(accountId, calendarPath);
1845
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 177 times.
177 if (calId < 0)
1846 ✗ return uids;
1847
1/1
✓ Branch 5 → 6 taken 177 times.
177 QSqlQuery q(m_db);
1848
1/1
✓ Branch 9 → 10 taken 177 times.
177 q.prepare(QStringLiteral("SELECT uid FROM calendar_outbox WHERE calendar_id = ?"));
1849
1/1
✓ Branch 14 → 15 taken 177 times.
177 q.addBindValue(calId);
1850
2/3
✓ Branch 16 → 17 taken 177 times.
✓ Branch 17 → 18 taken 177 times.
✗ Branch 17 → 27 not taken.
177 if (q.exec())
1851
3/3
✓ Branch 25 → 26 taken 180 times.
✓ Branch 26 → 19 taken 3 times.
✓ Branch 26 → 27 taken 177 times.
180 while (q.next())
1852
3/3
✓ Branch 19 → 20 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
3 uids.append(q.value(0).toString());
1853 177 return uids;
1854 177 }
1855
1856 // ═══════════════════════════════════════════════════════
1857 // Tasks
1858 // ═══════════════════════════════════════════════════════
1859
1860 62368 bool CalendarStore::writeTask(const CalendarTask &task) {
1861
1/1
✓ Branch 2 → 3 taken 62368 times.
62368 qint64 calId = calendarIdForPath(task.accountId, task.calendarPath);
1862
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 17 taken 62366 times.
62368 if (calId < 0) {
1863
5/5
✓ Branch 4 → 5 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 15 → 6 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
4 qCWarning(lcCalStore) << "upsertTask: calendar not found:"
1864
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
2 << task.accountId << task.calendarPath;
1865 2 return false;
1866 }
1867
1868
1/1
✓ Branch 17 → 18 taken 62366 times.
62366 QSqlQuery q(m_db);
1869
1/1
✓ Branch 21 → 22 taken 62366 times.
62366 q.prepare(QStringLiteral(
1870 "INSERT INTO tasks (calendar_id, uid, summary, description, due, "
1871 " percent_complete, priority, status, completed_at, etag, last_modified, "
1872 " dt_start, created, organizer, resource_href, due_all_day, related_message_id, ical_raw, tz_due, tz_start, start_all_day, parent_uid, rrule, tags, alarms, sort_order, previous_status, previous_percent) "
1873 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
1874 "ON CONFLICT(calendar_id, uid) DO UPDATE SET "
1875 " summary = excluded.summary,"
1876 " description = excluded.description,"
1877 " due = excluded.due,"
1878 " percent_complete = excluded.percent_complete,"
1879 " priority = excluded.priority,"
1880 " status = excluded.status,"
1881 " completed_at = excluded.completed_at,"
1882 " etag = excluded.etag,"
1883 " last_modified = excluded.last_modified,"
1884 " dt_start = excluded.dt_start,"
1885 " created = excluded.created,"
1886 " organizer = excluded.organizer,"
1887 " resource_href = excluded.resource_href,"
1888 " due_all_day = excluded.due_all_day,"
1889 " related_message_id = excluded.related_message_id, ical_raw = excluded.ical_raw, tz_due = excluded.tz_due, tz_start = excluded.tz_start, start_all_day = excluded.start_all_day, parent_uid = excluded.parent_uid, rrule = excluded.rrule, tags = excluded.tags, alarms = excluded.alarms, sort_order = excluded.sort_order, previous_status = excluded.previous_status, previous_percent = excluded.previous_percent"));
1890
1/1
✓ Branch 26 → 27 taken 62366 times.
62366 q.addBindValue(calId);
1891
1/1
✓ Branch 30 → 31 taken 62366 times.
62366 q.addBindValue(task.uid);
1892
1/1
✓ Branch 34 → 35 taken 62366 times.
62366 q.addBindValue(task.summary);
1893
1/1
✓ Branch 38 → 39 taken 62366 times.
62366 q.addBindValue(task.description);
1894
5/5
✓ Branch 41 → 42 taken 62366 times.
✓ Branch 42 → 43 taken 62042 times.
✓ Branch 42 → 45 taken 324 times.
✓ Branch 43 → 44 taken 62042 times.
✓ Branch 46 → 47 taken 62366 times.
62366 q.addBindValue(task.due.isValid() ? task.due.toSecsSinceEpoch()
1895 : QVariant());
1896
1/1
✓ Branch 50 → 51 taken 62366 times.
62366 q.addBindValue(task.percentComplete);
1897
1/1
✓ Branch 54 → 55 taken 62366 times.
62366 q.addBindValue(task.priority);
1898
1/1
✓ Branch 58 → 59 taken 62366 times.
62366 q.addBindValue(task.status);
1899
2/2
✓ Branch 61 → 62 taken 62366 times.
✓ Branch 66 → 67 taken 62366 times.
124732 q.addBindValue(task.completedAt.isValid()
1900
3/3
✓ Branch 62 → 63 taken 64 times.
✓ Branch 62 → 65 taken 62302 times.
✓ Branch 63 → 64 taken 64 times.
124732 ? task.completedAt.toSecsSinceEpoch()
1901 : QVariant());
1902
1/1
✓ Branch 70 → 71 taken 62366 times.
62366 q.addBindValue(task.etag);
1903
2/2
✓ Branch 73 → 74 taken 62366 times.
✓ Branch 78 → 79 taken 62366 times.
124732 q.addBindValue(task.lastModified.isValid()
1904
3/3
✓ Branch 74 → 75 taken 333 times.
✓ Branch 74 → 77 taken 62033 times.
✓ Branch 75 → 76 taken 333 times.
124732 ? task.lastModified.toSecsSinceEpoch()
1905 : QVariant());
1906
5/5
✓ Branch 81 → 82 taken 62366 times.
✓ Branch 82 → 83 taken 309 times.
✓ Branch 82 → 85 taken 62057 times.
✓ Branch 83 → 84 taken 309 times.
✓ Branch 86 → 87 taken 62366 times.
62366 q.addBindValue(task.dtStart.isValid() ? task.dtStart.toSecsSinceEpoch()
1907 : QVariant());
1908
5/5
✓ Branch 89 → 90 taken 62366 times.
✓ Branch 90 → 91 taken 349 times.
✓ Branch 90 → 93 taken 62017 times.
✓ Branch 91 → 92 taken 349 times.
✓ Branch 94 → 95 taken 62366 times.
62366 q.addBindValue(task.created.isValid() ? task.created.toSecsSinceEpoch()
1909 : QVariant());
1910
3/3
✓ Branch 98 → 99 taken 62365 times.
✓ Branch 98 → 100 taken 1 time.
✓ Branch 101 → 102 taken 62366 times.
62366 q.addBindValue(task.organizer.isEmpty() ? QVariant() : task.organizer);
1911
1/1
✓ Branch 105 → 106 taken 62366 times.
62366 q.addBindValue(task.resourceHref);
1912
3/3
✓ Branch 108 → 109 taken 61345 times.
✓ Branch 108 → 110 taken 1021 times.
✓ Branch 112 → 113 taken 62366 times.
62366 q.addBindValue(task.dueAllDay ? 1 : 0);
1913
3/3
✓ Branch 116 → 117 taken 62295 times.
✓ Branch 116 → 118 taken 71 times.
✓ Branch 119 → 120 taken 62366 times.
62437 q.addBindValue(task.relatedMessageId.isEmpty() ? QVariant()
1914 71 : task.relatedMessageId);
1915
1/1
✓ Branch 123 → 124 taken 62366 times.
62366 q.addBindValue(task.icalRaw);
1916
2/2
✓ Branch 126 → 127 taken 62366 times.
✓ Branch 128 → 129 taken 62366 times.
62366 q.addBindValue(ICalTime::zoneId(task.due, task.dueAllDay));
1917
2/2
✓ Branch 132 → 133 taken 62366 times.
✓ Branch 134 → 135 taken 62366 times.
62366 q.addBindValue(ICalTime::zoneId(task.dtStart, task.startAllDay));
1918
2/3
✗ Branch 138 → 139 not taken.
✓ Branch 138 → 140 taken 62366 times.
✓ Branch 142 → 143 taken 62366 times.
62366 q.addBindValue(task.startAllDay ? 1 : 0);
1919
1/1
✓ Branch 146 → 147 taken 62366 times.
62366 q.addBindValue(task.parentUid);
1920
1/1
✓ Branch 150 → 151 taken 62366 times.
62366 q.addBindValue(task.rrule);
1921
5/5
✓ Branch 153 → 154 taken 62366 times.
✓ Branch 154 → 155 taken 62366 times.
✓ Branch 155 → 156 taken 62366 times.
✓ Branch 156 → 157 taken 62366 times.
✓ Branch 158 → 159 taken 62366 times.
62366 q.addBindValue(QString::fromUtf8(QJsonDocument(QJsonArray::fromStringList(task.tags)).toJson(QJsonDocument::Compact)));
1922
2/2
✓ Branch 165 → 166 taken 62366 times.
✓ Branch 167 → 168 taken 62366 times.
62366 q.addBindValue(serializeDateList(task.alarmTimes));
1923
1/1
✓ Branch 172 → 173 taken 62366 times.
62366 q.addBindValue(task.sortOrder);
1924
1/1
✓ Branch 176 → 177 taken 62366 times.
62366 q.addBindValue(task.previousStatus);
1925
1/1
✓ Branch 180 → 181 taken 62366 times.
62366 q.addBindValue(task.previousPercent);
1926
3/3
✓ Branch 182 → 183 taken 62366 times.
✓ Branch 183 → 184 taken 3 times.
✓ Branch 183 → 200 taken 62363 times.
62366 if (!q.exec()) {
1927
8/8
✓ Branch 184 → 185 taken 3 times.
✓ Branch 188 → 189 taken 3 times.
✓ Branch 189 → 190 taken 3 times.
✓ Branch 190 → 191 taken 3 times.
✓ Branch 191 → 192 taken 3 times.
✓ Branch 192 → 193 taken 3 times.
✓ Branch 198 → 186 taken 3 times.
✓ Branch 198 → 199 taken 3 times.
6 qCWarning(lcCalStore) << "upsertTask:" << q.lastError().text();
1928 3 return false;
1929 }
1930
2/2
✓ Branch 201 → 202 taken 201 times.
✓ Branch 201 → 204 taken 62162 times.
62363 if (!task.resourceIcal.isEmpty())
1931
1/1
✓ Branch 202 → 203 taken 201 times.
201 return upsertResource(task.accountId, task.calendarPath, task.uid, task.resourceHref, task.etag, task.resourceIcal);
1932 62162 return true;
1933 62366 }
1934
1935 9 bool CalendarStore::moveTask(const CalendarTask &source,
1936 const CalendarTask &target) {
1937
4/8
✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 10 not taken.
✓ Branch 5 → 6 taken 9 times.
✗ Branch 5 → 10 not taken.
✓ Branch 7 → 8 taken 9 times.
✗ Branch 7 → 11 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 9 times.
27 if (source.uid.isEmpty() || source.uid != target.uid ||
1938
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 9 times.
18 (source.accountId == target.accountId &&
1939 9 source.calendarPath == target.calendarPath))
1940 ✗ return false;
1941
2/3
✓ Branch 14 → 15 taken 9 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 9 times.
9 if (!taskFor(source.uid, source.accountId, source.calendarPath))
1942 ✗ return false;
1943
2/3
✓ Branch 19 → 20 taken 9 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 9 times.
9 if (taskFor(target.uid, target.accountId, target.calendarPath))
1944 ✗ return false;
1945 // Through beginTransaction(): the writes below then nest as savepoints.
1946 9 const bool ownsTransaction = !m_inTransaction;
1947
2/6
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 28 taken 9 times.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 41 taken 9 times.
9 if (ownsTransaction && !beginTransaction()) {
1948 ✗ qCWarning(lcCalStore) << "moveTask: transaction failed";
1949 ✗ return false;
1950 }
1951 // Insert first. A failed target lookup/write can then roll back without
1952 // ever deleting the only durable local copy.
1953
2/4
✓ Branch 42 → 43 taken 9 times.
✗ Branch 42 → 45 not taken.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 51 taken 9 times.
18 if (!upsertTask(target) ||
1954
1/2
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 9 times.
9 !deleteTask(source.uid, source.accountId, source.calendarPath)) {
1955 ✗ if (ownsTransaction) rollbackTransaction();
1956 ✗ return false;
1957 }
1958
2/6
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 55 taken 9 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 73 taken 9 times.
9 if (ownsTransaction && !commitTransaction()) {
1959 ✗ qCWarning(lcCalStore) << "moveTask: commit:" << m_db.lastError().text();
1960 ✗ return false;
1961 }
1962 9 return true;
1963 }
1964
1965 // Helper: populate CalendarTask from query result row.
1966 // Columns: 0=id, 1=uid, 2=c.path, 3=c.accountId, 4=summary, ...
1967 1776145 static CalendarTask taskFromQuery(QSqlQuery &q) {
1968 1776145 CalendarTask task;
1969
2/2
✓ Branch 3 → 4 taken 1776145 times.
✓ Branch 4 → 5 taken 1776145 times.
1776145 task.id = q.value(0).toLongLong();
1970
2/2
✓ Branch 6 → 7 taken 1776145 times.
✓ Branch 7 → 8 taken 1776145 times.
1776145 task.uid = q.value(1).toString();
1971
2/2
✓ Branch 11 → 12 taken 1776145 times.
✓ Branch 12 → 13 taken 1776145 times.
1776145 task.calendarPath = q.value(2).toString();
1972
2/2
✓ Branch 16 → 17 taken 1776145 times.
✓ Branch 17 → 18 taken 1776145 times.
1776145 task.accountId = q.value(3).toString();
1973
2/2
✓ Branch 21 → 22 taken 1776145 times.
✓ Branch 22 → 23 taken 1776145 times.
1776145 task.summary = q.value(4).toString();
1974
2/2
✓ Branch 26 → 27 taken 1776145 times.
✓ Branch 27 → 28 taken 1776145 times.
1776145 task.description = q.value(5).toString();
1975
4/4
✓ Branch 31 → 32 taken 1776145 times.
✓ Branch 32 → 33 taken 1776145 times.
✓ Branch 34 → 35 taken 1773057 times.
✓ Branch 34 → 44 taken 3088 times.
1776145 if (!q.value(6).isNull())
1976
4/4
✓ Branch 35 → 36 taken 1773057 times.
✓ Branch 36 → 37 taken 1773057 times.
✓ Branch 37 → 38 taken 1773057 times.
✓ Branch 38 → 39 taken 1773057 times.
1773057 task.due = QDateTime::fromSecsSinceEpoch(q.value(6).toLongLong(), QTimeZone::utc());
1977
2/2
✓ Branch 44 → 45 taken 1776145 times.
✓ Branch 45 → 46 taken 1776145 times.
1776145 task.percentComplete = q.value(7).toInt();
1978
2/2
✓ Branch 47 → 48 taken 1776145 times.
✓ Branch 48 → 49 taken 1776145 times.
1776145 task.priority = q.value(8).toInt();
1979
2/2
✓ Branch 50 → 51 taken 1776145 times.
✓ Branch 51 → 52 taken 1776145 times.
1776145 task.status = q.value(9).toString();
1980
4/4
✓ Branch 55 → 56 taken 1776145 times.
✓ Branch 56 → 57 taken 1776145 times.
✓ Branch 58 → 59 taken 242 times.
✓ Branch 58 → 68 taken 1775903 times.
1776145 if (!q.value(10).isNull())
1981 task.completedAt =
1982
4/4
✓ Branch 59 → 60 taken 242 times.
✓ Branch 60 → 61 taken 242 times.
✓ Branch 61 → 62 taken 242 times.
✓ Branch 62 → 63 taken 242 times.
242 QDateTime::fromSecsSinceEpoch(q.value(10).toLongLong(), QTimeZone::utc());
1983
2/2
✓ Branch 68 → 69 taken 1776145 times.
✓ Branch 69 → 70 taken 1776145 times.
1776145 task.etag = q.value(11).toString();
1984
4/4
✓ Branch 73 → 74 taken 1776145 times.
✓ Branch 74 → 75 taken 1776145 times.
✓ Branch 76 → 77 taken 481 times.
✓ Branch 76 → 86 taken 1775664 times.
1776145 if (!q.value(12).isNull())
1985 task.lastModified =
1986
4/4
✓ Branch 77 → 78 taken 481 times.
✓ Branch 78 → 79 taken 481 times.
✓ Branch 79 → 80 taken 481 times.
✓ Branch 80 → 81 taken 481 times.
481 QDateTime::fromSecsSinceEpoch(q.value(12).toLongLong(), QTimeZone::utc());
1987
4/4
✓ Branch 86 → 87 taken 1776145 times.
✓ Branch 87 → 88 taken 1776145 times.
✓ Branch 89 → 90 taken 432 times.
✓ Branch 89 → 99 taken 1775713 times.
1776145 if (!q.value(13).isNull())
1988 task.dtStart =
1989
4/4
✓ Branch 90 → 91 taken 432 times.
✓ Branch 91 → 92 taken 432 times.
✓ Branch 92 → 93 taken 432 times.
✓ Branch 93 → 94 taken 432 times.
432 QDateTime::fromSecsSinceEpoch(q.value(13).toLongLong(), QTimeZone::utc());
1990
4/4
✓ Branch 99 → 100 taken 1776145 times.
✓ Branch 100 → 101 taken 1776145 times.
✓ Branch 102 → 103 taken 1535 times.
✓ Branch 102 → 112 taken 1774610 times.
1776145 if (!q.value(14).isNull())
1991 task.created =
1992
4/4
✓ Branch 103 → 104 taken 1535 times.
✓ Branch 104 → 105 taken 1535 times.
✓ Branch 105 → 106 taken 1535 times.
✓ Branch 106 → 107 taken 1535 times.
1535 QDateTime::fromSecsSinceEpoch(q.value(14).toLongLong(), QTimeZone::utc());
1993
2/2
✓ Branch 112 → 113 taken 1776145 times.
✓ Branch 113 → 114 taken 1776145 times.
1776145 task.organizer = q.value(15).toString();
1994
2/2
✓ Branch 117 → 118 taken 1776145 times.
✓ Branch 118 → 119 taken 1776145 times.
1776145 task.color = q.value(16).toString();
1995
2/2
✓ Branch 122 → 123 taken 1776145 times.
✓ Branch 123 → 124 taken 1776145 times.
1776145 task.calendarDisplayName = q.value(17).toString();
1996
2/2
✓ Branch 127 → 128 taken 1776145 times.
✓ Branch 128 → 129 taken 1776145 times.
1776145 task.resourceHref = q.value(18).toString();
1997
2/2
✓ Branch 132 → 133 taken 1776145 times.
✓ Branch 133 → 134 taken 1776145 times.
1776145 task.dueAllDay = q.value(19).toBool();
1998
2/2
✓ Branch 135 → 136 taken 1776145 times.
✓ Branch 136 → 137 taken 1776145 times.
1776145 task.relatedMessageId = q.value(20).toString();
1999
2/2
✓ Branch 140 → 141 taken 1776145 times.
✓ Branch 141 → 142 taken 1776145 times.
1776145 task.icalRaw = q.value(21).toString();
2000
11/15
✓ Branch 145 → 146 taken 1776145 times.
✓ Branch 146 → 147 taken 1773057 times.
✓ Branch 146 → 151 taken 3088 times.
✓ Branch 147 → 148 taken 1773057 times.
✓ Branch 148 → 149 taken 1773057 times.
✓ Branch 149 → 150 taken 1773057 times.
✓ Branch 150 → 152 taken 1773057 times.
✓ Branch 154 → 155 taken 1773057 times.
✓ Branch 154 → 156 taken 3088 times.
✓ Branch 156 → 157 taken 1773057 times.
✓ Branch 156 → 158 taken 3088 times.
✗ Branch 351 → 352 not taken.
✗ Branch 351 → 353 not taken.
✗ Branch 355 → 356 not taken.
✗ Branch 355 → 357 not taken.
1776145 task.due = task.due.isValid() ? ICalTime::fromStored(task.due.toSecsSinceEpoch(), q.value(22).toString()) : QDateTime{};
2001
11/15
✓ Branch 158 → 159 taken 1776145 times.
✓ Branch 159 → 160 taken 432 times.
✓ Branch 159 → 164 taken 1775713 times.
✓ Branch 160 → 161 taken 432 times.
✓ Branch 161 → 162 taken 432 times.
✓ Branch 162 → 163 taken 432 times.
✓ Branch 163 → 165 taken 432 times.
✓ Branch 167 → 168 taken 432 times.
✓ Branch 167 → 169 taken 1775713 times.
✓ Branch 169 → 170 taken 432 times.
✓ Branch 169 → 171 taken 1775713 times.
✗ Branch 360 → 361 not taken.
✗ Branch 360 → 362 not taken.
✗ Branch 364 → 365 not taken.
✗ Branch 364 → 366 not taken.
1776145 task.dtStart = task.dtStart.isValid() ? ICalTime::fromStored(task.dtStart.toSecsSinceEpoch(), q.value(23).toString()) : QDateTime{};
2002
2/2
✓ Branch 171 → 172 taken 1776145 times.
✓ Branch 172 → 173 taken 1776145 times.
1776145 task.startAllDay = q.value(24).toBool();
2003
2/2
✓ Branch 174 → 175 taken 1776145 times.
✓ Branch 175 → 176 taken 1776145 times.
1776145 task.parentUid = q.value(25).toString();
2004
2/2
✓ Branch 179 → 180 taken 1776145 times.
✓ Branch 180 → 181 taken 1776145 times.
1776145 task.rrule = q.value(26).toString();
2005
10/10
✓ Branch 184 → 185 taken 1776145 times.
✓ Branch 185 → 186 taken 1776145 times.
✓ Branch 186 → 187 taken 1776145 times.
✓ Branch 187 → 188 taken 1776145 times.
✓ Branch 191 → 192 taken 1776145 times.
✓ Branch 192 → 193 taken 1776145 times.
✓ Branch 196 → 197 taken 468 times.
✓ Branch 197 → 198 taken 468 times.
✓ Branch 202 → 194 taken 468 times.
✓ Branch 202 → 203 taken 1776145 times.
1776613 for (const auto &tag : QJsonDocument::fromJson(q.value(27).toByteArray()).array()) task.tags << tag.toString();
2006
3/3
✓ Branch 204 → 205 taken 1776145 times.
✓ Branch 205 → 206 taken 1776145 times.
✓ Branch 206 → 207 taken 1776145 times.
1776145 task.alarmTimes = deserializeDateList(q.value(28).toString());
2007
2/2
✓ Branch 211 → 212 taken 1776145 times.
✓ Branch 212 → 213 taken 1776145 times.
1776145 task.sortOrder = q.value(29).toLongLong();
2008
4/4
✓ Branch 214 → 215 taken 1776145 times.
✓ Branch 215 → 216 taken 1776145 times.
✓ Branch 219 → 220 taken 1776145 times.
✓ Branch 220 → 221 taken 1776145 times.
1776145 task.previousStatus = q.value(30).toString(); task.previousPercent = q.value(31).toInt();
2009
4/4
✓ Branch 222 → 223 taken 1776145 times.
✓ Branch 223 → 224 taken 1776145 times.
✓ Branch 227 → 228 taken 1776145 times.
✓ Branch 228 → 229 taken 1776145 times.
1776145 task.resourceIcal = q.value(32).toString(); task.syncState = q.value(33).toString();
2010
2/2
✓ Branch 232 → 233 taken 1776145 times.
✓ Branch 233 → 234 taken 1776145 times.
1776145 task.readOnly = q.value(34).toBool();
2011 1776145 return task;
2012 ✗ }
2013
2014 static const char *kTaskSelect =
2015 "SELECT t.id, t.uid, c.path, c.accountId, t.summary, t.description, t.due, "
2016 " t.percent_complete, t.priority, t.status, t.completed_at, "
2017 " t.etag, t.last_modified, t.dt_start, t.created, "
2018 " t.organizer, c.color, c.displayName, t.resource_href, t.due_all_day, "
2019 " t.related_message_id, t.ical_raw, t.tz_due, t.tz_start, t.start_all_day, t.parent_uid, t.rrule, t.tags, t.alarms, t.sort_order, t.previous_status, t.previous_percent, r.ical, COALESCE(o.state, ''), c.readOnly "
2020 "FROM tasks t JOIN calendars c ON t.calendar_id = c.id "
2021 "LEFT JOIN calendar_resources r ON r.calendar_id = t.calendar_id AND r.uid = t.uid "
2022 "LEFT JOIN calendar_outbox o ON o.calendar_id = t.calendar_id AND o.uid = t.uid ";
2023
2024 // List projection: same column positions as kTaskSelect, raw iCalendar
2025 // columns replaced by NULL and the resource join dropped (G9).
2026 static const char *kTaskListSelect =
2027 "SELECT t.id, t.uid, c.path, c.accountId, t.summary, t.description, t.due, "
2028 " t.percent_complete, t.priority, t.status, t.completed_at, "
2029 " t.etag, t.last_modified, t.dt_start, t.created, "
2030 " t.organizer, c.color, c.displayName, t.resource_href, t.due_all_day, "
2031 " t.related_message_id, NULL, t.tz_due, t.tz_start, t.start_all_day, t.parent_uid, t.rrule, t.tags, "
2032 " t.alarms, t.sort_order, t.previous_status, t.previous_percent, NULL, COALESCE(o.state, ''), c.readOnly "
2033 "FROM tasks t JOIN calendars c ON t.calendar_id = c.id "
2034 "LEFT JOIN calendar_outbox o ON o.calendar_id = t.calendar_id AND o.uid = t.uid ";
2035
2036 2147 QList<CalendarTask> CalendarStore::listTasks() const {
2037 2147 QList<CalendarTask> result;
2038
1/1
✓ Branch 2 → 3 taken 2147 times.
2147 QSqlQuery q(m_db);
2039
1/1
✓ Branch 3 → 4 taken 2147 times.
2147 q.setForwardOnly(true);
2040
4/5
✓ Branch 8 → 9 taken 2147 times.
✓ Branch 9 → 10 taken 2147 times.
✓ Branch 10 → 11 taken 2147 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 32 taken 2147 times.
2147 if (!q.exec(QString::fromLatin1(kTaskListSelect) + QStringLiteral("ORDER BY t.due ASC NULLS LAST, t.priority DESC")))
2041 ✗ qCWarning(lcCalStore) << "listTasks:" << q.lastError().text();
2042
3/3
✓ Branch 37 → 38 taken 127503 times.
✓ Branch 38 → 33 taken 125356 times.
✓ Branch 38 → 39 taken 2147 times.
127503 while (q.next())
2043
2/2
✓ Branch 33 → 34 taken 125356 times.
✓ Branch 34 → 35 taken 125356 times.
125356 result.append(taskFromQuery(q));
2044 2147 return result;
2045 2147 }
2046
2047 980 QSet<QString> CalendarStore::openTaskMessageIds() const {
2048 980 QSet<QString> result;
2049
1/1
✓ Branch 3 → 4 taken 980 times.
980 QSqlQuery q(m_db);
2050
1/1
✓ Branch 4 → 5 taken 980 times.
980 q.setForwardOnly(true);
2051
2/3
✓ Branch 8 → 9 taken 980 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 28 taken 980 times.
980 if (!q.exec(QStringLiteral("SELECT DISTINCT related_message_id FROM tasks WHERE related_message_id IS NOT NULL "
2052 "AND related_message_id != '' AND status NOT IN ('COMPLETED', 'CANCELLED')")))
2053 ✗ qCWarning(lcCalStore) << "openTaskMessageIds:" << q.lastError().text();
2054
6/6
✓ Branch 29 → 30 taken 80 times.
✓ Branch 30 → 31 taken 80 times.
✓ Branch 31 → 32 taken 80 times.
✓ Branch 35 → 36 taken 1060 times.
✓ Branch 36 → 29 taken 80 times.
✓ Branch 36 → 37 taken 980 times.
1060 while (q.next()) result.insert(q.value(0).toString());
2055 980 return result;
2056 980 }
2057
2058 336 QList<CalendarTask> CalendarStore::tasksForMessage(const QString &messageId) const {
2059 336 QList<CalendarTask> result;
2060
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 336 times.
336 if (messageId.isEmpty()) return result;
2061
1/1
✓ Branch 5 → 6 taken 336 times.
336 QSqlQuery q(m_db);
2062
3/3
✓ Branch 10 → 11 taken 336 times.
✓ Branch 11 → 12 taken 336 times.
✓ Branch 12 → 13 taken 336 times.
336 q.prepare(QString::fromLatin1(kTaskListSelect) + QStringLiteral("WHERE t.related_message_id = :id ORDER BY t.due ASC NULLS LAST"));
2063
1/1
✓ Branch 22 → 23 taken 336 times.
672 q.bindValue(QStringLiteral(":id"), messageId);
2064
2/11
✓ Branch 26 → 27 taken 336 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 44 taken 336 times.
✗ Branch 28 → 29 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 34 → 35 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 42 → 30 not taken.
✗ Branch 42 → 43 not taken.
336 if (!q.exec()) qCWarning(lcCalStore) << "tasksForMessage:" << q.lastError().text();
2065
5/5
✓ Branch 45 → 46 taken 18 times.
✓ Branch 46 → 47 taken 18 times.
✓ Branch 49 → 50 taken 354 times.
✓ Branch 50 → 45 taken 18 times.
✓ Branch 50 → 51 taken 336 times.
354 while (q.next()) result.append(taskFromQuery(q));
2066 336 return result;
2067 336 }
2068
2069 102 QList<CalendarTask> CalendarStore::allTasks() const {
2070 102 QList<CalendarTask> result;
2071
1/1
✓ Branch 2 → 3 taken 102 times.
102 QSqlQuery q(m_db);
2072
2/2
✓ Branch 7 → 8 taken 102 times.
✓ Branch 9 → 10 taken 102 times.
204 q.exec(QString::fromLatin1(kTaskSelect) +
2073
1/1
✓ Branch 8 → 9 taken 102 times.
306 QStringLiteral("ORDER BY t.due ASC NULLS LAST, t.priority DESC"));
2074
3/3
✓ Branch 19 → 20 taken 915518 times.
✓ Branch 20 → 15 taken 915416 times.
✓ Branch 20 → 21 taken 102 times.
915518 while (q.next())
2075
2/2
✓ Branch 15 → 16 taken 915416 times.
✓ Branch 16 → 17 taken 915416 times.
915416 result.append(taskFromQuery(q));
2076 102 return result;
2077 102 }
2078
2079 820 std::optional<CalendarTask> CalendarStore::taskFor(const QString &uid,
2080 const QString &accountId,
2081 const QString &calendarPath) const {
2082
1/1
✓ Branch 2 → 3 taken 820 times.
820 QSqlQuery q(m_db);
2083
2/2
✓ Branch 7 → 8 taken 820 times.
✓ Branch 9 → 10 taken 820 times.
1640 q.prepare(QString::fromLatin1(kTaskSelect) +
2084
1/1
✓ Branch 8 → 9 taken 820 times.
2460 QStringLiteral("WHERE t.uid=:uid AND c.accountId=:account "
2085 "AND c.path=:path LIMIT 1"));
2086
1/1
✓ Branch 19 → 20 taken 820 times.
1640 q.bindValue(QStringLiteral(":uid"), uid);
2087
1/1
✓ Branch 28 → 29 taken 820 times.
1640 q.bindValue(QStringLiteral(":account"), accountId);
2088
1/1
✓ Branch 37 → 38 taken 820 times.
1640 q.bindValue(QStringLiteral(":path"), calendarPath);
2089
7/8
✓ Branch 41 → 42 taken 820 times.
✓ Branch 42 → 43 taken 820 times.
✗ Branch 42 → 45 not taken.
✓ Branch 43 → 44 taken 820 times.
✓ Branch 44 → 45 taken 82 times.
✓ Branch 44 → 46 taken 738 times.
✓ Branch 47 → 48 taken 82 times.
✓ Branch 47 → 49 taken 738 times.
820 if (!q.exec() || !q.next())
2090 82 return std::nullopt;
2091
1/1
✓ Branch 49 → 50 taken 738 times.
738 return taskFromQuery(q);
2092 820 }
2093
2094 1272 QList<CalendarTask> CalendarStore::openTasks() const {
2095 1272 QList<CalendarTask> result;
2096
1/1
✓ Branch 2 → 3 taken 1272 times.
1272 QSqlQuery q(m_db);
2097
2/2
✓ Branch 7 → 8 taken 1272 times.
✓ Branch 9 → 10 taken 1272 times.
2544 q.exec(QString::fromLatin1(kTaskSelect) +
2098
1/1
✓ Branch 8 → 9 taken 1272 times.
3816 QStringLiteral(
2099 "WHERE t.status != 'COMPLETED' AND t.status != 'CANCELLED' "
2100 "ORDER BY t.due ASC NULLS LAST, t.priority DESC"));
2101
3/3
✓ Branch 19 → 20 taken 735537 times.
✓ Branch 20 → 15 taken 734265 times.
✓ Branch 20 → 21 taken 1272 times.
735537 while (q.next())
2102
2/2
✓ Branch 15 → 16 taken 734265 times.
✓ Branch 16 → 17 taken 734265 times.
734265 result.append(taskFromQuery(q));
2103 1272 return result;
2104 1272 }
2105
2106 QList<CalendarTask>
2107 8 CalendarStore::tasksForCalendar(const QString &calendarPath) const {
2108
1/1
✓ Branch 3 → 4 taken 8 times.
8 return tasksForCalendar(QString(), calendarPath);
2109 }
2110
2111 QList<CalendarTask>
2112 120 CalendarStore::tasksForCalendar(const QString &accountId,
2113 const QString &calendarPath) const {
2114 120 QList<CalendarTask> result;
2115
1/1
✓ Branch 2 → 3 taken 120 times.
120 qint64 calId = calendarIdForPath(accountId, calendarPath);
2116
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 117 times.
120 if (calId < 0)
2117 3 return result;
2118
2119
1/1
✓ Branch 5 → 6 taken 117 times.
117 QSqlQuery q(m_db);
2120
2/2
✓ Branch 10 → 11 taken 117 times.
✓ Branch 12 → 13 taken 117 times.
234 q.prepare(QString::fromLatin1(kTaskSelect) +
2121
1/1
✓ Branch 11 → 12 taken 117 times.
351 QStringLiteral(
2122 "WHERE t.calendar_id = ? "
2123 "ORDER BY t.due ASC NULLS LAST, t.priority DESC"));
2124
1/1
✓ Branch 19 → 20 taken 117 times.
117 q.addBindValue(calId);
2125
3/3
✓ Branch 21 → 22 taken 117 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 116 times.
117 if (!q.exec())
2126 1 return result;
2127
2128
3/3
✓ Branch 29 → 30 taken 459 times.
✓ Branch 30 → 25 taken 343 times.
✓ Branch 30 → 31 taken 116 times.
459 while (q.next())
2129
2/2
✓ Branch 25 → 26 taken 343 times.
✓ Branch 26 → 27 taken 343 times.
343 result.append(taskFromQuery(q));
2130 116 return result;
2131 117 }
2132
2133 2 bool CalendarStore::removeStaleTasks(const QString &calendarPath,
2134 const QStringList &activeUids,
2135 bool allowEmpty) {
2136
1/1
✓ Branch 3 → 4 taken 2 times.
2 return removeStaleTasks(QString(), calendarPath, activeUids, allowEmpty);
2137 }
2138
2139 12 bool CalendarStore::removeStaleTasks(const QString &accountId,
2140 const QString &calendarPath,
2141 const QStringList &activeUids,
2142 bool allowEmpty) {
2143
1/1
✓ Branch 2 → 3 taken 12 times.
12 qint64 calId = calendarIdForPath(accountId, calendarPath);
2144
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 11 times.
12 if (calId < 0)
2145 1 return false;
2146
2147
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 35 taken 5 times.
11 if (activeUids.isEmpty()) {
2148 // SEC-2026-07-21-17: same guard as removeStaleEvents.
2149
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 20 taken 6 times.
6 if (!allowEmpty) {
2150 ✗ qCWarning(lcCalStore)
2151 ✗ << "removeStaleTasks: activeUids is empty — skipping delete"
2152 ✗ << "(use allowEmpty=true to force)";
2153 ✗ return true;
2154 }
2155
1/1
✓ Branch 20 → 21 taken 6 times.
6 QSqlQuery q(m_db);
2156
1/1
✓ Branch 24 → 25 taken 6 times.
6 q.prepare(QStringLiteral("DELETE FROM tasks WHERE calendar_id = ?"));
2157
1/1
✓ Branch 29 → 30 taken 6 times.
6 q.addBindValue(calId);
2158
1/1
✓ Branch 31 → 32 taken 6 times.
6 return q.exec();
2159 6 }
2160
2161 5 QString error;
2162 5 bool ok = true;
2163
4/6
✓ Branch 45 → 46 taken 5 times.
✓ Branch 45 → 47 taken 5 times.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 60 taken 4 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
10 if (!DatabaseSync::removeRowsMissingFromUidSet(
2164
1/1
✓ Branch 40 → 41 taken 5 times.
5 m_db,
2165 10 QStringLiteral(
2166 "DELETE FROM tasks WHERE calendar_id = ? "
2167 "AND NOT EXISTS (SELECT 1 FROM mailjd_active_sync_uids active "
2168 "WHERE active.uid = tasks.uid)"),
2169 {calId}, activeUids, &error)) {
2170
6/6
✓ Branch 48 → 49 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 58 → 50 taken 1 time.
✓ Branch 58 → 59 taken 1 time.
2 qCWarning(lcCalStore) << "removeStaleTasks:" << error;
2171 1 ok = false;
2172 }
2173 5 return ok;
2174 10 }
2175
2176 4 bool CalendarStore::deleteTask(const QString &uid,
2177 const QString &calendarPath) {
2178
1/1
✓ Branch 3 → 4 taken 4 times.
4 return deleteTask(uid, QString(), calendarPath);
2179 }
2180
2181 54 bool CalendarStore::deleteTask(const QString &uid,
2182 const QString &accountId,
2183 const QString &calendarPath) {
2184
1/1
✓ Branch 2 → 3 taken 54 times.
54 qint64 calId = calendarIdForPath(accountId, calendarPath);
2185
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 16 taken 51 times.
54 if (calId < 0) {
2186
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(lcCalStore) << "deleteTask: calendar not found:" << calendarPath;
2187 3 return false;
2188 }
2189
1/1
✓ Branch 16 → 17 taken 51 times.
51 QSqlQuery q(m_db);
2190
1/1
✓ Branch 20 → 21 taken 51 times.
51 q.prepare(
2191 102 QStringLiteral("DELETE FROM tasks WHERE uid = ? AND calendar_id = ?"));
2192
1/1
✓ Branch 25 → 26 taken 51 times.
51 q.addBindValue(uid);
2193
1/1
✓ Branch 29 → 30 taken 51 times.
51 q.addBindValue(calId);
2194
3/3
✓ Branch 31 → 32 taken 51 times.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 49 taken 50 times.
51 if (!q.exec()) {
2195
8/8
✓ Branch 33 → 34 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 47 → 35 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
2 qCWarning(lcCalStore) << "deleteTask:" << q.lastError().text();
2196 1 return false;
2197 }
2198
1/1
✓ Branch 49 → 50 taken 50 times.
50 return q.numRowsAffected() > 0;
2199 51 }
2200
2201 // ═══════════════════════════════════════════════════════
2202 // Task Queries (Sprint 37 – T-451)
2203 // ═══════════════════════════════════════════════════════
2204
2205 3 QMap<QString, int> CalendarStore::openTaskCountByCalendar() const {
2206 3 QMap<QString, int> result;
2207
1/1
✓ Branch 2 → 3 taken 3 times.
3 QSqlQuery q(m_db);
2208
1/1
✓ Branch 6 → 7 taken 3 times.
3 q.exec(QStringLiteral(
2209 "SELECT c.path, COUNT(*) FROM tasks t "
2210 "JOIN calendars c ON t.calendar_id = c.id "
2211 "WHERE t.status != 'COMPLETED' AND t.status != 'CANCELLED' "
2212 "GROUP BY c.path"));
2213
3/3
✓ Branch 19 → 20 taken 5 times.
✓ Branch 20 → 10 taken 2 times.
✓ Branch 20 → 21 taken 3 times.
5 while (q.next())
2214
5/5
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 14 → 15 taken 2 times.
2 result.insert(q.value(0).toString(), q.value(1).toInt());
2215 3 return result;
2216 3 }
2217
2218 QList<CalendarTask>
2219 7 CalendarStore::searchTasks(const QString &query,
2220 bool includeCompleted) const {
2221 7 QList<CalendarTask> result;
2222
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 6 times.
7 if (query.isEmpty())
2223 1 return result;
2224
2225 // SEC-2026-07-21-22: Escape LIKE wildcards in user input.
2226 6 QString escapedQuery = query;
2227
1/1
✓ Branch 11 → 12 taken 6 times.
6 escapedQuery.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
2228
1/1
✓ Branch 19 → 20 taken 6 times.
6 escapedQuery.replace(QLatin1Char('%'), QStringLiteral("\\%"));
2229
1/1
✓ Branch 27 → 28 taken 6 times.
6 escapedQuery.replace(QLatin1Char('_'), QStringLiteral("\\_"));
2230 QString likePattern =
2231
2/2
✓ Branch 34 → 35 taken 6 times.
✓ Branch 35 → 36 taken 6 times.
6 QLatin1Char('%') + escapedQuery + QLatin1Char('%');
2232
2233
1/1
✓ Branch 37 → 38 taken 6 times.
6 QSqlQuery q(m_db);
2234
1/1
✓ Branch 42 → 43 taken 6 times.
12 QString sql = QString::fromLatin1(kTaskSelect) +
2235
1/1
✓ Branch 43 → 44 taken 6 times.
18 QStringLiteral(
2236 "WHERE (t.summary LIKE ? ESCAPE '\\' OR "
2237 "t.description LIKE ? ESCAPE '\\') ");
2238
2/2
✓ Branch 47 → 48 taken 4 times.
✓ Branch 47 → 55 taken 2 times.
6 if (!includeCompleted)
2239
1/1
✓ Branch 51 → 52 taken 4 times.
4 sql += QStringLiteral(
2240 "AND t.status != 'COMPLETED' AND t.status != 'CANCELLED' ");
2241
1/1
✓ Branch 58 → 59 taken 6 times.
6 sql += QStringLiteral("ORDER BY t.due ASC NULLS LAST, t.priority DESC");
2242
2243
1/1
✓ Branch 61 → 62 taken 6 times.
6 q.prepare(sql);
2244
1/1
✓ Branch 64 → 65 taken 6 times.
6 q.addBindValue(likePattern);
2245
1/1
✓ Branch 68 → 69 taken 6 times.
6 q.addBindValue(likePattern);
2246
3/3
✓ Branch 70 → 71 taken 6 times.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 5 times.
6 if (!q.exec())
2247 1 return result;
2248
2249
3/3
✓ Branch 78 → 79 taken 10 times.
✓ Branch 79 → 74 taken 5 times.
✓ Branch 79 → 80 taken 5 times.
10 while (q.next())
2250
2/2
✓ Branch 74 → 75 taken 5 times.
✓ Branch 75 → 76 taken 5 times.
5 result.append(taskFromQuery(q));
2251 5 return result;
2252 6 }
2253
2254 4 QList<CalendarTask> CalendarStore::urgentTasks() const {
2255 4 QList<CalendarTask> result;
2256
1/1
✓ Branch 2 → 3 taken 4 times.
4 QSqlQuery q(m_db);
2257
2258 // Tasks due within 7 days (including overdue)
2259
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 4 → 5 taken 4 times.
4 qint64 now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
2260 4 qint64 sevenDays = now + 7 * 24 * 3600;
2261
2262
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
8 q.prepare(QString::fromLatin1(kTaskSelect) +
2263
1/1
✓ Branch 11 → 12 taken 4 times.
12 QStringLiteral(
2264 "WHERE t.due IS NOT NULL AND t.due <= ? "
2265 "AND t.status != 'COMPLETED' AND t.status != 'CANCELLED' "
2266 "ORDER BY t.due ASC"));
2267
1/1
✓ Branch 19 → 20 taken 4 times.
4 q.addBindValue(sevenDays);
2268
3/3
✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 2 times.
4 if (!q.exec())
2269 2 return result;
2270
2271
3/3
✓ Branch 29 → 30 taken 3 times.
✓ Branch 30 → 25 taken 1 time.
✓ Branch 30 → 31 taken 2 times.
3 while (q.next())
2272
2/2
✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
1 result.append(taskFromQuery(q));
2273 2 return result;
2274 4 }
2275
2276 6 QList<CalendarTask> CalendarStore::completedTasks() const {
2277 6 QList<CalendarTask> result;
2278
1/1
✓ Branch 2 → 3 taken 6 times.
6 QSqlQuery q(m_db);
2279
2/2
✓ Branch 7 → 8 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
12 q.exec(QString::fromLatin1(kTaskSelect) +
2280
1/1
✓ Branch 8 → 9 taken 6 times.
18 QStringLiteral(
2281 "WHERE t.status = 'COMPLETED' "
2282 "ORDER BY t.completed_at DESC NULLS LAST"));
2283
3/3
✓ Branch 19 → 20 taken 9 times.
✓ Branch 20 → 15 taken 3 times.
✓ Branch 20 → 21 taken 6 times.
9 while (q.next())
2284
2/2
✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
3 result.append(taskFromQuery(q));
2285 6 return result;
2286 6 }
2287
2288 // ─── Sprint 107 D-21: conflict history ─────────────────────
2289
2290 107 bool CalendarStore::addConflictHistory(const ConflictHistory::Entry &entry, qint64 now) {
2291 107 return ConflictHistory::add(m_db, entry, now);
2292 }
2293
2294 899 QList<ConflictHistory::Entry> CalendarStore::conflictHistory(int limit) const {
2295 899 return ConflictHistory::list(m_db, limit);
2296 }
2297
2298 4 std::optional<ConflictHistory::Entry> CalendarStore::conflictHistoryEntry(qint64 id) const {
2299 4 return ConflictHistory::find(m_db, id);
2300 }
2301
2302 3 bool CalendarStore::removeConflictHistory(qint64 id) { return ConflictHistory::remove(m_db, id); }
2303