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 |