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: 100.0% 1 / 0 / 1
Functions: 100.0% 1 / 0 / 1
Branches: -% 0 / 0 / 0

data/CalendarStore.h
Line Branch Exec Source
1 #pragma once
2
3 #include <functional>
4
5 #include <QList>
6 #include <QSet>
7 #include <QMap>
8 #include <QObject>
9 #include <QSqlDatabase>
10 #include <QString>
11 #include <optional>
12
13 #include "CalendarModels.h"
14 #include "CalendarPeopleIndex.h"
15 #include "ConflictHistory.h"
16
17 // ═══════════════════════════════════════════════════════
18 // CalendarStore — SQLite persistence for calendars, events, tasks
19 // (Sprint 32 – T-332)
20 // Architecture mirrors ContactStore (Sprint 16).
21 // ═══════════════════════════════════════════════════════
22
23 class CalendarStore : public QObject {
24 Q_OBJECT
25 public:
26 static constexpr int SchemaVersion = 16; // v15: Sprint 103 event_people; v16: Sprint 107 conflict_history
27
28 explicit CalendarStore(QObject *parent = nullptr);
29 ~CalendarStore() override;
30
31 bool open(const QString &dbPath);
32 void close();
33 bool isOpen() const;
34 // Sprint 105 S105.5.4: SQLite's data_version of this connection — it
35 // changes when another connection committed (cache stamps of readers).
36 qint64 dataVersion() const;
37 // Path this store was opened with; empty when closed. Workers use it to
38 // open their own connections (T-87.C4).
39 821 QString databasePath() const { return m_dbPath; }
40
41 // --- Calendars ---
42 void upsertCalendar(const CalendarInfo &cal, const QString &accountId);
43 QList<CalendarInfo> allCalendars() const;
44 // Sprint 93 S93.6.1: the CTag of the last complete sync of a collection
45 // ("" = never completed). Written only after events and tasks of that
46 // collection were stored, so an interrupted sync never skips changes.
47 QString calendarCtag(const QString &accountId, const QString &path) const;
48 bool setCalendarCtag(const QString &accountId, const QString &path, const QString &ctag);
49 // Sprint 93 S93.6.2: RFC 6578 sync-token of the last applied
50 // sync-collection ("" = none; the next sync downloads everything).
51 QString calendarSyncToken(const QString &accountId, const QString &path) const;
52 bool setCalendarSyncToken(const QString &accountId, const QString &path, const QString &token);
53 // UID stored for a DAV resource href (events, then tasks); "" = unknown.
54 QString uidForHref(const QString &accountId, const QString &path, const QString &href) const;
55 // Removes every event / task row and the resource row of a removed href.
56 // False on an SQL failure (callers roll back their transaction).
57 bool removeHref(const QString &accountId, const QString &path, const QString &href);
58 // --- Sync enumeration checkpoint (Sprint 96 S96.3.0, CalendarStoreSync.cpp) ---
59 // A token-less sync-collection may arrive in several pages (RFC 6578
60 // 507). Until its last page is applied the collection is "enumerating":
61 // `token` is the page checkpoint to continue from, the seen UIDs persist
62 // and nothing is pruned. The committed sync token stays untouched.
63 struct EnumerationState { bool active = false; QString token; };
64 EnumerationState enumerationState(const QString &accountId, const QString &path) const;
65 bool beginEnumeration(const QString &accountId, const QString &path);
66 bool addEnumerationSeen(const QString &accountId, const QString &path, const QStringList &uids);
67 bool setEnumerationCheckpoint(const QString &accountId, const QString &path, const QString &token);
68 // Prunes events, tasks and resources whose UID was neither seen nor is in
69 // `keepUids`, then clears the state. An enumeration that saw nothing
70 // prunes nothing (SEC-2026-07-21-17 empty-response guard).
71 bool finishEnumeration(const QString &accountId, const QString &path,
72 const QStringList &keepUids, bool completeEmpty = false);
73 bool abandonEnumeration(const QString &accountId, const QString &path);
74 // Removes the account's calendars missing from `activePaths`; an empty
75 // list removes nothing (S106-149). The whole account goes through
76 // removeAccountCalendars().
77 void removeCalendarsNotIn(const QString &accountId,
78 const QStringList &activePaths);
79 void removeAccountCalendars(const QString &accountId);
80 QString accountIdForCalendarPath(const QString &path) const;
81 void setCalendarColor(const QString &path, const QString &color);
82 void setCalendarColor(const QString &accountId, const QString &path,
83 const QString &color);
84 QString calendarColor(const QString &path) const;
85 QString calendarColor(const QString &accountId, const QString &path) const;
86 bool beginTransaction();
87 bool commitTransaction();
88 void rollbackTransaction();
89
90 // Mutating single-record methods return false on SQL/admission failure;
91 // deletion also returns false when no matching row exists.
92 // --- Events ---
93 bool upsertEvent(const CalendarEvent &event);
94 // Single-record lookups for write-back (editors, toggle, rollback).
95 std::optional<CalendarEvent> eventFor(const QString &uid, const QString &accountId,
96 const QString &calendarPath) const;
97 // Display query: no raw iCalendar text (cheap for large ranges).
98 QList<CalendarEvent> eventsForDateRange(const QDateTime &from,
99 const QDateTime &to) const;
100 // Rows of that range marked TRANSP:TRANSPARENT (they do not make the user
101 // busy); the display query carries no raw text to tell.
102 QSet<qint64> transparentEventIds(const QDateTime &from, const QDateTime &to) const;
103 // Write-back lookups carry icalRaw and resourceIcal (Sprint 93 S93.1.1).
104 // An invalid recurrenceId addresses the series master.
105 std::optional<CalendarEvent> occurrenceEvent(const QString &uid, const QString &accountId,
106 const QString &calendarPath,
107 const QDateTime &recurrenceId) const;
108 // Sprint 93 S93.5.1: local reminder state per occurrence (never synced).
109 // occurrence = the occurrence's start (UTC ISO); dismissed reminders do
110 // not fire again, snoozed ones fire at `snoozedUntil` (epoch seconds).
111 struct ReminderState { bool dismissed = false; qint64 snoozedUntil = 0; qint64 firedAt = 0; };
112 ReminderState reminderState(const QString &accountId, const QString &path, const QString &uid,
113 const QString &occurrence) const;
114 bool setReminderState(const QString &accountId, const QString &path, const QString &uid,
115 const QString &occurrence, const ReminderState &state);
116 // Sprint 93 S93.5.5 / F93-16: every row (master and exceptions) of the
117 // resources whose summary, location or description contain all `words`
118 // (case-insensitive, Unicode); a word with:<name> / mit:<name> needs a
119 // person of the event (event_people name or address, Sprint 103 D-21)
120 // containing <name>; at most `limit` resources.
121 QList<CalendarEvent> searchEvents(const QStringList &words, int limit = 300) const;
122 // Sprint 93 S93.5.2: the calendars (accountId, path) that hold `uid`
123 // (an invitation that was already answered or stored).
124 QList<QPair<QString, QString>> calendarsWithUid(const QString &uid) const;
125 // Master and every exception of one UID (one DAV resource).
126 QList<CalendarEvent> eventSeries(const QString &uid, const QString &accountId,
127 const QString &calendarPath) const;
128 // The VCALENDAR text of a DAV resource as last seen from / sent to the server.
129 bool upsertResource(const QString &accountId, const QString &calendarPath,
130 const QString &uid, const QString &href, const QString &etag,
131 const QString &ical);
132 QString resourceFor(const QString &accountId, const QString &calendarPath,
133 const QString &uid) const;
134 bool removeResource(const QString &accountId, const QString &calendarPath,
135 const QString &uid);
136
137 // --- Write outbox (Sprint 93 S93.1.6) ---
138 // One entry per DAV resource whose local state must reach the server.
139 // Returns the entry's revision after the change (0 on failure).
140 int enqueueOutbox(const QString &accountId, const QString &calendarPath,
141 const QString &uid, const QString &operation,
142 const QString &href, const QString &baseEtag);
143 QList<CalendarOutboxEntry> outboxEntries() const;
144 // Sprint 107 D-21: the losing side of conflict resolutions (ConflictHistory).
145 bool addConflictHistory(const ConflictHistory::Entry &entry, qint64 now);
146 QList<ConflictHistory::Entry> conflictHistory(int limit = ConflictHistory::kLimit) const;
147 std::optional<ConflictHistory::Entry> conflictHistoryEntry(qint64 id) const;
148 bool removeConflictHistory(qint64 id);
149 std::optional<CalendarOutboxEntry> outboxEntry(const QString &accountId,
150 const QString &calendarPath,
151 const QString &uid) const;
152 bool updateOutbox(const CalendarOutboxEntry &entry);
153 // revision > 0 removes only that revision (a newer local change keeps it).
154 bool removeOutbox(const QString &accountId, const QString &calendarPath,
155 const QString &uid, int revision = 0);
156 QStringList outboxUids(const QString &accountId, const QString &calendarPath) const;
157 QList<CalendarEvent> eventsForCalendar(const QString &calendarPath) const;
158 // Sprint 103 S103.6.1: the CONTACT people of every event, keyed by
159 // CalendarPeopleIndex::eventKey (agenda avatars; CalendarPeopleIndex.cpp).
160 QHash<QString, QList<EventPerson>> namedPeopleByEvent() const;
161 // Sprint 103 S103.6.2: events naming one of the card UIDs (CONTACT) or with
162 // one of the addresses as attendee/organizer (CalendarPeopleIndex.cpp).
163 QList<CalendarPeopleIndex::EventRef> eventsOfPerson(const QStringList &cardUids, const QStringList &addresses,
164 int limit = 200) const;
165 QList<CalendarEvent> eventsForCalendar(const QString &accountId,
166 const QString &calendarPath) const;
167 // False on an SQL failure; an empty list without allowEmpty is a no-op.
168 bool removeStaleEvents(const QString &calendarPath,
169 const QStringList &activeUids,
170 bool allowEmpty = false);
171 bool removeStaleEvents(const QString &accountId, const QString &calendarPath,
172 const QStringList &activeUids,
173 bool allowEmpty = false);
174 bool deleteEvent(const QString &uid, const QString &calendarPath);
175 // A valid recurrenceId deletes exactly that exception; the default
176 // deletes the master and cascades over its exceptions.
177 bool deleteEvent(const QString &uid, const QString &accountId,
178 const QString &calendarPath,
179 const QDateTime &recurrenceId = {});
180
181 // --- Tasks ---
182 bool upsertTask(const CalendarTask &task);
183 // Atomically replace a task identity when it moves between calendars.
184 // `source` must still exist and `target` must use the same UID. The target
185 // resource identity is expected to be cleared by the caller.
186 bool moveTask(const CalendarTask &source, const CalendarTask &target);
187 std::optional<CalendarTask> taskFor(const QString &uid, const QString &accountId,
188 const QString &calendarPath) const;
189 QList<CalendarTask> allTasks() const;
190 // All tasks for list views: identical fields except icalRaw and
191 // resourceIcal, which stay empty (no raw resource is read or joined).
192 // Writers must re-read a task with taskFor() before saving it.
193 QList<CalendarTask> listTasks() const;
194 // S94.5.5: canonical Message-IDs that open tasks link to (mail list glyph)
195 // and the tasks linked to one mail (reader strip); indexed lookups.
196 QSet<QString> openTaskMessageIds() const;
197 QList<CalendarTask> tasksForMessage(const QString &messageId) const;
198 QList<CalendarTask> openTasks() const;
199 QList<CalendarTask> tasksForCalendar(const QString &calendarPath) const;
200 QList<CalendarTask> tasksForCalendar(const QString &accountId,
201 const QString &calendarPath) const;
202 bool removeStaleTasks(const QString &calendarPath,
203 const QStringList &activeUids,
204 bool allowEmpty = false);
205 bool removeStaleTasks(const QString &accountId, const QString &calendarPath,
206 const QStringList &activeUids,
207 bool allowEmpty = false);
208 bool deleteTask(const QString &uid, const QString &calendarPath);
209 bool deleteTask(const QString &uid, const QString &accountId,
210 const QString &calendarPath);
211
212 // --- Task queries (Sprint 37 – T-451) ---
213 QMap<QString, int> openTaskCountByCalendar() const;
214 QList<CalendarTask> searchTasks(const QString &query,
215 bool includeCompleted = false) const;
216 QList<CalendarTask> urgentTasks() const;
217 QList<CalendarTask> completedTasks() const;
218
219 private:
220 bool writeEvent(const CalendarEvent &event); // upsertEvent() inside its write unit
221 bool writeTask(const CalendarTask &task);
222 bool pruneReminderState(qint64 calendarId);
223 bool inSavepoint(const std::function<bool()> &write);
224 bool createSchema();
225 void normalizeDefaultPortHrefs();
226 QStringList tableColumns(const QString &table) const;
227 QString migrationBackupPath(int version) const;
228 void backupBeforeMigration(int version);
229 void removeMigrationBackup(int version);
230 // v13 (CAL-PERF-YEAR): series_end of rows stored before the column existed.
231 bool backfillSeriesEnds();
232 qint64 calendarIdForPath(const QString &path) const;
233 qint64 calendarIdForPath(const QString &accountId,
234 const QString &path) const;
235
236 QSqlDatabase m_db;
237 QString m_connectionName;
238 QString m_dbPath;
239 bool m_inTransaction = false;
240 };
241