Branch data Line data Source code
1 : : #include "CalendarStore.h"
2 : : #include "DatabaseSecurity.h"
3 : : #include "DatabaseSync.h"
4 : :
5 : : #include "ui/ThemeManager.h"
6 : :
7 : : #include <QLoggingCategory>
8 : : #include <QSqlError>
9 : : #include <QSqlQuery>
10 : : #include <QUuid>
11 : :
12 [ + + + - : 539 : Q_LOGGING_CATEGORY(lcCalStore, "mailjd.calendarstore")
+ - - - ]
13 : :
14 : 2124 : static bool execMigrationStatement(QSqlQuery &q, const QString &statement,
15 : : const QString &context) {
16 [ + + ]: 2124 : if (q.exec(statement))
17 : 2120 : return true;
18 [ + - + - : 8 : qCWarning(lcCalStore) << context << q.lastError().text() << statement;
+ - + - +
- + - + -
+ + ]
19 : 4 : return false;
20 : : }
21 : :
22 : 322 : static bool runCalendarMigration(QSqlDatabase &db, QSqlQuery &q,
23 : : int targetVersion,
24 : : const QString &context,
25 : : const QStringList &statements) {
26 [ - + ]: 322 : if (!db.transaction()) {
27 [ # # # # : 0 : qCWarning(lcCalStore) << context << "transaction:"
# # # # #
# ]
28 [ # # # # : 0 : << db.lastError().text();
# # ]
29 : 0 : return false;
30 : : }
31 : :
32 [ + + ]: 2124 : for (const QString &statement : statements) {
33 [ + - + + ]: 1806 : if (!execMigrationStatement(q, statement, context)) {
34 [ + - ]: 4 : db.rollback();
35 : 4 : return false;
36 : : }
37 : : }
38 : :
39 [ + - ]: 318 : if (!execMigrationStatement(
40 [ + - - + ]: 954 : q, QStringLiteral("PRAGMA user_version = %1").arg(targetVersion),
41 : : context)) {
42 : 0 : db.rollback();
43 : 0 : return false;
44 : : }
45 : :
46 [ - + ]: 318 : if (!db.commit()) {
47 [ # # # # : 0 : qCWarning(lcCalStore) << context << "commit:" << db.lastError().text();
# # # # #
# # # # #
# # ]
48 : 0 : return false;
49 : : }
50 : 318 : return true;
51 : : }
52 : :
53 [ + - ]: 129 : CalendarStore::CalendarStore(QObject *parent) : QObject(parent) {}
54 : :
55 : 150 : CalendarStore::~CalendarStore() { close(); }
56 : :
57 : 124 : bool CalendarStore::open(const QString &dbPath) {
58 [ + - + + ]: 124 : if (!DatabaseSecurity::preparePath(dbPath)) {
59 [ + - + - : 2 : qCWarning(lcCalStore)
+ + ]
60 [ + - + - ]: 1 : << "Failed to create private calendar DB:" << dbPath;
61 : 1 : return false;
62 : : }
63 : : m_connectionName =
64 : 246 : QStringLiteral("calstore_") +
65 [ + - + - : 369 : QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
+ - + - ]
66 [ + - ]: 246 : m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"),
67 [ + - ]: 246 : m_connectionName);
68 [ + - ]: 123 : m_db.setDatabaseName(dbPath);
69 [ + - + + ]: 123 : if (!m_db.open()) {
70 [ + - + - : 2 : qCWarning(lcCalStore) << "Failed to open DB:" << m_db.lastError().text();
+ - + - +
- + - +
+ ]
71 : 1 : return false;
72 : : }
73 [ + - - + ]: 122 : if (!DatabaseSecurity::restrictExistingFile(dbPath)) {
74 [ # # # # : 0 : qCWarning(lcCalStore)
# # ]
75 [ # # # # ]: 0 : << "Failed to restrict calendar DB permissions:" << dbPath;
76 [ # # ]: 0 : m_db.close();
77 : 0 : return false;
78 : : }
79 : : // WAL mode for concurrent reads
80 [ + - ]: 122 : QSqlQuery pragma(m_db);
81 [ + - ]: 122 : pragma.exec(QStringLiteral("PRAGMA journal_mode=WAL"));
82 [ + - ]: 122 : pragma.exec(QStringLiteral("PRAGMA busy_timeout=5000"));
83 [ + - ]: 122 : pragma.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
84 : :
85 [ + - ]: 122 : return createSchema();
86 : 122 : }
87 : :
88 : 153 : void CalendarStore::close() {
89 [ + + ]: 153 : if (m_db.isOpen())
90 : 109 : m_db.close();
91 : : // T-402/Bug 20: Release reference before removeDatabase
92 [ + - + - ]: 153 : m_db = QSqlDatabase();
93 [ + + ]: 153 : if (!m_connectionName.isEmpty()) {
94 : 110 : QSqlDatabase::removeDatabase(m_connectionName);
95 : 110 : m_connectionName.clear();
96 : : }
97 : 153 : }
98 : :
99 : 278 : bool CalendarStore::isOpen() const { return m_db.isOpen(); }
100 : :
101 : 122 : bool CalendarStore::createSchema() {
102 [ + - ]: 122 : QSqlQuery q(m_db);
103 : :
104 [ + - ]: 122 : bool ok = q.exec(QStringLiteral(
105 : : "CREATE TABLE IF NOT EXISTS calendars ("
106 : : " id INTEGER PRIMARY KEY,"
107 : : " path TEXT UNIQUE NOT NULL,"
108 : : " displayName TEXT,"
109 : : " color TEXT,"
110 : : " ctag TEXT,"
111 : : " accountId TEXT,"
112 : : " readOnly INTEGER DEFAULT 0"
113 : : ")"));
114 [ + + ]: 122 : if (!ok) {
115 [ + - + - : 2 : qCWarning(lcCalStore) << "calendars table:" << q.lastError().text();
+ - + - +
- + - +
+ ]
116 : 1 : return false;
117 : : }
118 : :
119 [ + - ]: 121 : ok = q.exec(QStringLiteral(
120 : : "CREATE TABLE IF NOT EXISTS events ("
121 : : " id INTEGER PRIMARY KEY,"
122 : : " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
123 : : " uid TEXT NOT NULL,"
124 : : " summary TEXT,"
125 : : " description TEXT,"
126 : : " location TEXT,"
127 : : " dt_start INTEGER,"
128 : : " dt_end INTEGER,"
129 : : " all_day INTEGER DEFAULT 0,"
130 : : " rrule TEXT,"
131 : : " etag TEXT,"
132 : : " last_modified INTEGER,"
133 : : " UNIQUE(calendar_id, uid)"
134 : : ")"));
135 [ + + ]: 121 : if (!ok) {
136 [ + - + - : 2 : qCWarning(lcCalStore) << "events table:" << q.lastError().text();
+ - + - +
- + - +
+ ]
137 : 1 : return false;
138 : : }
139 : :
140 [ + - ]: 120 : ok = q.exec(QStringLiteral(
141 : : "CREATE TABLE IF NOT EXISTS tasks ("
142 : : " id INTEGER PRIMARY KEY,"
143 : : " calendar_id INTEGER REFERENCES calendars(id) ON DELETE CASCADE,"
144 : : " uid TEXT NOT NULL,"
145 : : " summary TEXT,"
146 : : " description TEXT,"
147 : : " due INTEGER,"
148 : : " percent_complete INTEGER DEFAULT 0,"
149 : : " priority INTEGER DEFAULT 0,"
150 : : " status TEXT DEFAULT 'NEEDS-ACTION',"
151 : : " completed_at INTEGER,"
152 : : " etag TEXT,"
153 : : " last_modified INTEGER,"
154 : : " UNIQUE(calendar_id, uid)"
155 : : ")"));
156 [ + + ]: 120 : if (!ok) {
157 [ + - + - : 2 : qCWarning(lcCalStore) << "tasks table:" << q.lastError().text();
+ - + - +
- + - +
+ ]
158 : 1 : return false;
159 : : }
160 : :
161 : : // Index for date-range queries on events
162 [ + - ]: 119 : q.exec(QStringLiteral(
163 : : "CREATE INDEX IF NOT EXISTS idx_events_dates ON events(dt_start, dt_end)"));
164 : :
165 : : // --- Schema migration (Sprint 37 – T-451) ---
166 : : // Check user_version for migrations
167 : 119 : int version = 0;
168 [ + - + - : 238 : if (q.exec(QStringLiteral("PRAGMA user_version")) && q.next())
+ - + - +
- + - + -
- - - - ]
169 [ + - + - ]: 119 : version = q.value(0).toInt();
170 : :
171 [ + + ]: 119 : if (version < 1) {
172 [ + - + - : 214 : qCInfo(lcCalStore) << "Migrating schema v0 → v1 (adding dt_start, created, organizer)";
+ - + + ]
173 [ + + - - ]: 428 : if (!runCalendarMigration(
174 [ + - ]: 107 : m_db, q, 1, QStringLiteral("calendar v1 migration:"),
175 : : {
176 [ + + ]: 107 : QStringLiteral("ALTER TABLE tasks ADD COLUMN dt_start INTEGER"),
177 : 107 : QStringLiteral("ALTER TABLE tasks ADD COLUMN created INTEGER"),
178 : 107 : QStringLiteral("ALTER TABLE tasks ADD COLUMN organizer TEXT"),
179 : : })) {
180 : 1 : return false;
181 : : }
182 : 106 : version = 1;
183 : : }
184 : :
185 [ + + ]: 118 : if (version < 2) {
186 [ + - + - : 214 : qCInfo(lcCalStore)
+ + ]
187 [ + - ]: 107 : << "Migrating schema v1 → v2 (calendar identity includes accountId)";
188 [ + - ]: 107 : q.exec(QStringLiteral("PRAGMA foreign_keys=OFF"));
189 : : const QStringList statements = {
190 : 0 : QStringLiteral(
191 : : "CREATE TABLE calendars_new ("
192 : : " id INTEGER PRIMARY KEY,"
193 : : " path TEXT NOT NULL,"
194 : : " displayName TEXT,"
195 : : " color TEXT,"
196 : : " ctag TEXT,"
197 : : " accountId TEXT NOT NULL DEFAULT '',"
198 : : " readOnly INTEGER DEFAULT 0,"
199 : : " UNIQUE(accountId, path)"
200 : : ")"),
201 : 107 : QStringLiteral(
202 : : "INSERT INTO calendars_new "
203 : : "(id, path, displayName, color, ctag, accountId, readOnly) "
204 : : "SELECT id, path, displayName, color, ctag, "
205 : : "COALESCE(accountId, ''), readOnly FROM calendars"),
206 : 107 : QStringLiteral(
207 : : "CREATE TABLE events_new ("
208 : : " id INTEGER PRIMARY KEY,"
209 : : " calendar_id INTEGER REFERENCES calendars_new(id) ON DELETE CASCADE,"
210 : : " uid TEXT NOT NULL,"
211 : : " summary TEXT,"
212 : : " description TEXT,"
213 : : " location TEXT,"
214 : : " dt_start INTEGER,"
215 : : " dt_end INTEGER,"
216 : : " all_day INTEGER DEFAULT 0,"
217 : : " rrule TEXT,"
218 : : " etag TEXT,"
219 : : " last_modified INTEGER,"
220 : : " UNIQUE(calendar_id, uid)"
221 : : ")"),
222 : 107 : QStringLiteral(
223 : : "INSERT INTO events_new "
224 : : "(id, calendar_id, uid, summary, description, location, dt_start, "
225 : : "dt_end, all_day, rrule, etag, last_modified) "
226 : : "SELECT id, calendar_id, uid, summary, description, location, "
227 : : "dt_start, dt_end, all_day, rrule, etag, last_modified FROM events"),
228 : 107 : QStringLiteral(
229 : : "CREATE TABLE tasks_new ("
230 : : " id INTEGER PRIMARY KEY,"
231 : : " calendar_id INTEGER REFERENCES calendars_new(id) ON DELETE CASCADE,"
232 : : " uid TEXT NOT NULL,"
233 : : " summary TEXT,"
234 : : " description TEXT,"
235 : : " due INTEGER,"
236 : : " percent_complete INTEGER DEFAULT 0,"
237 : : " priority INTEGER DEFAULT 0,"
238 : : " status TEXT DEFAULT 'NEEDS-ACTION',"
239 : : " completed_at INTEGER,"
240 : : " etag TEXT,"
241 : : " last_modified INTEGER,"
242 : : " dt_start INTEGER,"
243 : : " created INTEGER,"
244 : : " organizer TEXT,"
245 : : " UNIQUE(calendar_id, uid)"
246 : : ")"),
247 : 107 : QStringLiteral(
248 : : "INSERT INTO tasks_new "
249 : : "(id, calendar_id, uid, summary, description, due, "
250 : : "percent_complete, priority, status, completed_at, etag, "
251 : : "last_modified, dt_start, created, organizer) "
252 : : "SELECT id, calendar_id, uid, summary, description, due, "
253 : : "percent_complete, priority, status, completed_at, etag, "
254 : : "last_modified, dt_start, created, organizer FROM tasks"),
255 : 107 : QStringLiteral("DROP TABLE tasks"),
256 : 107 : QStringLiteral("DROP TABLE events"),
257 : 107 : QStringLiteral("DROP TABLE calendars"),
258 : 107 : QStringLiteral("ALTER TABLE calendars_new RENAME TO calendars"),
259 : 107 : QStringLiteral("ALTER TABLE events_new RENAME TO events"),
260 : 107 : QStringLiteral("ALTER TABLE tasks_new RENAME TO tasks"),
261 [ + + - - ]: 1498 : };
262 : :
263 [ + - ]: 107 : if (!runCalendarMigration(m_db, q, 2,
264 [ + + ]: 214 : QStringLiteral("calendar v2 migration:"),
265 : : statements)) {
266 [ + - ]: 1 : q.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
267 : 1 : return false;
268 : : }
269 [ + - ]: 106 : q.exec(QStringLiteral("PRAGMA foreign_keys=ON"));
270 : 106 : version = 2;
271 [ + + ]: 107 : }
272 : :
273 [ + + ]: 117 : if (version < 3) {
274 [ + - + - : 216 : qCInfo(lcCalStore)
+ + ]
275 [ + - ]: 108 : << "Migrating schema v2 → v3 (storing CalDAV resource hrefs)";
276 [ + + - - ]: 324 : if (!runCalendarMigration(
277 [ + - ]: 108 : m_db, q, 3, QStringLiteral("calendar v3 migration:"),
278 : : {
279 [ + + ]: 108 : QStringLiteral("ALTER TABLE events ADD COLUMN resource_href TEXT"),
280 : 108 : QStringLiteral("ALTER TABLE tasks ADD COLUMN resource_href TEXT"),
281 : : })) {
282 : 2 : return false;
283 : : }
284 : 106 : version = 3;
285 : : }
286 : :
287 [ + - ]: 115 : q.exec(QStringLiteral(
288 : : "CREATE INDEX IF NOT EXISTS idx_events_dates ON events(dt_start, dt_end)"));
289 : :
290 [ + - + - : 230 : qCDebug(lcCalStore) << "Schema created/verified (version:" << qMax(version, 3) << ")";
+ - + - +
- + + ]
291 : 115 : return true;
292 [ + - + - : 2265 : }
+ - - - -
- - - - -
- - - - ]
293 : :
294 : 151 : qint64 CalendarStore::calendarIdForPath(const QString &path) const {
295 [ + - ]: 151 : QSqlQuery q(m_db);
296 [ + - ]: 151 : q.prepare(QStringLiteral("SELECT id FROM calendars WHERE path = ? "
297 : : "ORDER BY id LIMIT 2"));
298 [ + - ]: 151 : q.addBindValue(path);
299 [ + - + + : 151 : if (!q.exec() || !q.next())
+ - + + +
+ ]
300 : 32 : return -1;
301 [ + - + - ]: 119 : const qint64 id = q.value(0).toLongLong();
302 [ + - + + ]: 119 : if (q.next()) {
303 [ + - + - : 10 : qCWarning(lcCalStore)
+ + ]
304 [ + - + - ]: 5 : << "calendarIdForPath: ambiguous path without accountId:" << path;
305 : 5 : return -1;
306 : : }
307 : 114 : return id;
308 : 151 : }
309 : :
310 : 387 : qint64 CalendarStore::calendarIdForPath(const QString &accountId,
311 : : const QString &path) const {
312 [ + + ]: 387 : if (accountId.isEmpty())
313 [ + - ]: 99 : return calendarIdForPath(path);
314 : :
315 [ + - ]: 288 : QSqlQuery q(m_db);
316 [ + - ]: 288 : q.prepare(QStringLiteral(
317 : : "SELECT id FROM calendars WHERE accountId = ? AND path = ?"));
318 [ + - ]: 288 : q.addBindValue(accountId);
319 [ + - ]: 288 : q.addBindValue(path);
320 [ + - + + : 288 : if (q.exec() && q.next())
+ - + + +
+ ]
321 [ + - + - ]: 281 : return q.value(0).toLongLong();
322 : 7 : return -1;
323 : 288 : }
324 : :
325 : : // ═══════════════════════════════════════════════════════
326 : : // Calendars
327 : : // ═══════════════════════════════════════════════════════
328 : :
329 : 130 : void CalendarStore::upsertCalendar(const CalendarInfo &cal,
330 : : const QString &accountId) {
331 [ + + ]: 130 : const QString owner = accountId.isEmpty() ? cal.accountId : accountId;
332 : : // Determine color: use server color, keep existing custom color, or generate
333 : : // a deterministic palette color for new calendars without server color
334 : 130 : QString color = cal.color;
335 [ + + ]: 130 : if (color.isEmpty()) {
336 [ + - ]: 46 : QString existing = calendarColor(owner, cal.path);
337 [ + + ]: 46 : if (existing.isEmpty()) {
338 : : // Shared palette (67.B3: ThemeManager owns all color decisions)
339 [ + - ]: 45 : const QStringList palette = ThemeManager::calendarPalette();
340 : 45 : color = palette.at(
341 [ + - + - ]: 45 : qHash(owner + QLatin1Char('\0') + cal.path) % palette.size());
342 : 45 : }
343 : 46 : }
344 : :
345 [ + - ]: 130 : QSqlQuery q(m_db);
346 [ + - ]: 130 : q.prepare(QStringLiteral(
347 : : "INSERT INTO calendars (path, displayName, color, ctag, accountId, readOnly) "
348 : : "VALUES (?, ?, ?, ?, ?, ?) "
349 : : "ON CONFLICT(accountId, path) DO UPDATE SET "
350 : : " displayName = excluded.displayName,"
351 : : " ctag = excluded.ctag,"
352 : : " readOnly = excluded.readOnly"));
353 [ + - ]: 130 : q.addBindValue(cal.path);
354 [ + - ]: 130 : q.addBindValue(cal.displayName);
355 [ + - ]: 130 : q.addBindValue(color);
356 [ + - ]: 130 : q.addBindValue(cal.ctag);
357 [ + - ]: 130 : q.addBindValue(owner);
358 [ + + + - ]: 130 : q.addBindValue(cal.readOnly ? 1 : 0);
359 [ + - + + ]: 130 : if (!q.exec())
360 [ + - + - : 2 : qCWarning(lcCalStore) << "upsertCalendar:" << q.lastError().text();
+ - + - +
- + - +
+ ]
361 : 130 : }
362 : :
363 : 117 : QList<CalendarInfo> CalendarStore::allCalendars() const {
364 : 117 : QList<CalendarInfo> result;
365 [ + - ]: 117 : QSqlQuery q(m_db);
366 [ + - ]: 117 : q.exec(QStringLiteral(
367 : : "SELECT path, accountId, displayName, color, ctag, readOnly "
368 : : "FROM calendars"));
369 [ + - + + ]: 310 : while (q.next()) {
370 : 193 : CalendarInfo cal;
371 [ + - + - ]: 193 : cal.path = q.value(0).toString();
372 [ + - + - ]: 193 : cal.accountId = q.value(1).toString();
373 [ + - + - ]: 193 : cal.displayName = q.value(2).toString();
374 [ + - + - ]: 193 : cal.color = q.value(3).toString();
375 [ + - + - ]: 193 : cal.ctag = q.value(4).toString();
376 [ + - + - ]: 193 : cal.readOnly = q.value(5).toBool();
377 [ + - ]: 193 : result.append(cal);
378 : 193 : }
379 : 117 : return result;
380 : 117 : }
381 : :
382 : 5 : void CalendarStore::removeCalendarsNotIn(const QString &accountId,
383 : : const QStringList &activePaths) {
384 [ + + ]: 5 : if (activePaths.isEmpty()) {
385 [ + - ]: 2 : QSqlQuery q(m_db);
386 [ + - ]: 2 : q.prepare(QStringLiteral("DELETE FROM calendars WHERE accountId = ?"));
387 [ + - ]: 2 : q.addBindValue(accountId);
388 [ + - ]: 2 : q.exec();
389 : 2 : return;
390 : 2 : }
391 : : // Build placeholder string
392 : 3 : QStringList placeholders;
393 [ + + ]: 7 : for (int i = 0; i < activePaths.size(); ++i)
394 [ + - ]: 4 : placeholders << QStringLiteral("?");
395 : :
396 [ + - ]: 3 : QSqlQuery q(m_db);
397 [ + - ]: 9 : q.prepare(QStringLiteral("DELETE FROM calendars WHERE accountId = ? "
398 : : "AND path NOT IN (%1)")
399 [ + - + - ]: 9 : .arg(placeholders.join(QStringLiteral(","))));
400 [ + - ]: 3 : q.addBindValue(accountId);
401 [ + + ]: 7 : for (const auto &p : activePaths)
402 [ + - ]: 4 : q.addBindValue(p);
403 [ + - ]: 3 : q.exec();
404 : 3 : }
405 : :
406 : 15 : QString CalendarStore::accountIdForCalendarPath(const QString &path) const {
407 [ + - ]: 15 : QSqlQuery q(m_db);
408 [ + - ]: 15 : q.prepare(QStringLiteral("SELECT accountId FROM calendars WHERE path = ? "
409 : : "ORDER BY id LIMIT 2"));
410 [ + - ]: 15 : q.addBindValue(path);
411 [ + - + + : 15 : if (!q.exec() || !q.next())
+ - + + +
+ ]
412 : 8 : return {};
413 [ + - + - ]: 7 : const QString accountId = q.value(0).toString();
414 [ + - + + ]: 7 : if (q.next()) {
415 [ + - + - : 4 : qCWarning(lcCalStore)
+ + ]
416 [ + - ]: 2 : << "accountIdForCalendarPath: ambiguous path without accountId:"
417 [ + - ]: 2 : << path;
418 : 2 : return {};
419 : : }
420 : 5 : return accountId;
421 : 15 : }
422 : :
423 : 21 : void CalendarStore::setCalendarColor(const QString &path,
424 : : const QString &color) {
425 [ + - ]: 21 : const qint64 calId = calendarIdForPath(path);
426 [ + + ]: 21 : if (calId < 0)
427 : 4 : return;
428 : :
429 [ + - ]: 17 : QSqlQuery q(m_db);
430 [ + - ]: 17 : q.prepare(QStringLiteral("UPDATE calendars SET color = ? WHERE id = ?"));
431 [ + - ]: 17 : q.addBindValue(color);
432 [ + - ]: 17 : q.addBindValue(calId);
433 [ + - + + ]: 17 : if (!q.exec())
434 [ + - + - : 2 : qCWarning(lcCalStore) << "setCalendarColor:" << q.lastError().text();
+ - + - +
- + - +
+ ]
435 : : else
436 [ + - + - : 32 : qCDebug(lcCalStore) << "setCalendarColor:" << path << "→" << color
+ - + - +
- + - +
+ ]
437 [ + - + - : 16 : << "rows:" << q.numRowsAffected();
+ - ]
438 : 17 : }
439 : :
440 : 4 : void CalendarStore::setCalendarColor(const QString &accountId,
441 : : const QString &path,
442 : : const QString &color) {
443 [ + - ]: 4 : QSqlQuery q(m_db);
444 [ + - ]: 4 : q.prepare(QStringLiteral(
445 : : "UPDATE calendars SET color = ? WHERE accountId = ? AND path = ?"));
446 [ + - ]: 4 : q.addBindValue(color);
447 [ + - ]: 4 : q.addBindValue(accountId);
448 [ + - ]: 4 : q.addBindValue(path);
449 [ + - + + ]: 4 : if (!q.exec())
450 [ + - + - : 2 : qCWarning(lcCalStore) << "setCalendarColor:" << q.lastError().text();
+ - + - +
- + - +
+ ]
451 : 4 : }
452 : :
453 : 31 : QString CalendarStore::calendarColor(const QString &path) const {
454 [ + - ]: 31 : const qint64 calId = calendarIdForPath(path);
455 [ + + ]: 31 : if (calId < 0) {
456 [ + - + - : 12 : qCDebug(lcCalStore) << "calendarColor:" << path << "= (not found)";
+ - + - +
- + + ]
457 : 6 : return {};
458 : : }
459 : :
460 [ + - ]: 25 : QSqlQuery q(m_db);
461 [ + - ]: 25 : q.prepare(QStringLiteral("SELECT color FROM calendars WHERE id = ?"));
462 [ + - ]: 25 : q.addBindValue(calId);
463 [ + - + + : 25 : if (q.exec() && q.next()) {
+ - + + +
+ ]
464 [ + - + - ]: 23 : QString c = q.value(0).toString();
465 [ + - + - : 46 : qCDebug(lcCalStore) << "calendarColor:" << path << "=" << c;
+ - + - +
- + - +
+ ]
466 : 23 : return c;
467 : 23 : }
468 [ + - + - : 4 : qCDebug(lcCalStore) << "calendarColor:" << path << "= (not found)";
+ - + - +
- + + ]
469 : 2 : return {};
470 : 25 : }
471 : :
472 : 54 : QString CalendarStore::calendarColor(const QString &accountId,
473 : : const QString &path) const {
474 [ + - ]: 54 : QSqlQuery q(m_db);
475 [ + - ]: 54 : q.prepare(QStringLiteral(
476 : : "SELECT color FROM calendars WHERE accountId = ? AND path = ?"));
477 [ + - ]: 54 : q.addBindValue(accountId);
478 [ + - ]: 54 : q.addBindValue(path);
479 [ + - + + : 54 : if (q.exec() && q.next())
+ - + + +
+ ]
480 [ + - + - ]: 7 : return q.value(0).toString();
481 : 47 : return {};
482 : 54 : }
483 : :
484 : 5 : void CalendarStore::beginTransaction() {
485 [ + + ]: 5 : if (m_inTransaction) return;
486 : 4 : m_inTransaction = m_db.transaction();
487 : : }
488 : :
489 : 6 : void CalendarStore::commitTransaction() {
490 [ + + ]: 6 : if (!m_inTransaction) return;
491 : 4 : m_db.commit();
492 : 4 : m_inTransaction = false;
493 : : }
494 : :
495 : : // ═══════════════════════════════════════════════════════
496 : : // Events
497 : : // ═══════════════════════════════════════════════════════
498 : :
499 : 101 : void CalendarStore::upsertEvent(const CalendarEvent &event) {
500 [ + - ]: 101 : qint64 calId = calendarIdForPath(event.accountId, event.calendarPath);
501 [ + + ]: 101 : if (calId < 0) {
502 [ + - + - : 12 : qCWarning(lcCalStore) << "upsertEvent: calendar not found:"
+ - + + ]
503 [ + - + - ]: 6 : << event.accountId << event.calendarPath;
504 : 6 : return;
505 : : }
506 : :
507 [ + - ]: 95 : QSqlQuery q(m_db);
508 [ + - ]: 95 : q.prepare(QStringLiteral(
509 : : "INSERT INTO events (calendar_id, uid, summary, description, location, "
510 : : " dt_start, dt_end, all_day, rrule, etag, last_modified, resource_href) "
511 : : "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
512 : : "ON CONFLICT(calendar_id, uid) DO UPDATE SET "
513 : : " summary = excluded.summary,"
514 : : " description = excluded.description,"
515 : : " location = excluded.location,"
516 : : " dt_start = excluded.dt_start,"
517 : : " dt_end = excluded.dt_end,"
518 : : " all_day = excluded.all_day,"
519 : : " rrule = excluded.rrule,"
520 : : " etag = excluded.etag,"
521 : : " last_modified = excluded.last_modified,"
522 : : " resource_href = excluded.resource_href"));
523 [ + - ]: 95 : q.addBindValue(calId);
524 [ + - ]: 95 : q.addBindValue(event.uid);
525 [ + - ]: 95 : q.addBindValue(event.summary);
526 [ + - ]: 95 : q.addBindValue(event.description);
527 [ + - ]: 95 : q.addBindValue(event.location);
528 [ + - + + : 95 : q.addBindValue(event.dtStart.isValid() ? event.dtStart.toSecsSinceEpoch()
+ - + - ]
529 : : : QVariant());
530 [ + - + + : 95 : q.addBindValue(event.dtEnd.isValid() ? event.dtEnd.toSecsSinceEpoch()
+ - + - ]
531 : : : QVariant());
532 [ + + + - ]: 95 : q.addBindValue(event.allDay ? 1 : 0);
533 [ + - ]: 95 : q.addBindValue(event.rrule);
534 [ + - ]: 95 : q.addBindValue(event.etag);
535 [ + - + - ]: 190 : q.addBindValue(event.lastModified.isValid()
536 [ + + + - ]: 190 : ? event.lastModified.toSecsSinceEpoch()
537 : : : QVariant());
538 [ + - ]: 95 : q.addBindValue(event.resourceHref);
539 [ + - + + ]: 95 : if (!q.exec())
540 [ + - + - : 2 : qCWarning(lcCalStore) << "upsertEvent:" << q.lastError().text();
+ - + - +
- + - +
+ ]
541 : 95 : }
542 : :
543 : : QList<CalendarEvent>
544 : 99 : CalendarStore::eventsForDateRange(const QDateTime &from,
545 : : const QDateTime &to) const {
546 : 99 : QList<CalendarEvent> result;
547 [ + - ]: 99 : QSqlQuery q(m_db);
548 [ + - ]: 99 : q.prepare(QStringLiteral(
549 : : "SELECT e.id, e.uid, c.path, c.accountId, e.summary, e.description, e.location, "
550 : : " e.dt_start, e.dt_end, e.all_day, e.rrule, e.etag, e.last_modified, "
551 : : " c.color, e.resource_href "
552 : : "FROM events e JOIN calendars c ON e.calendar_id = c.id "
553 : : "WHERE (e.dt_start <= ? AND (e.dt_end >= ? OR e.dt_end IS NULL)) "
554 : : " OR (e.rrule != '' AND e.dt_start <= ?) "
555 : : "ORDER BY e.dt_start ASC"));
556 [ + - + - ]: 99 : q.addBindValue(to.toSecsSinceEpoch());
557 [ + - + - ]: 99 : q.addBindValue(from.toSecsSinceEpoch());
558 [ + - + - ]: 99 : q.addBindValue(to.toSecsSinceEpoch());
559 [ + - + + ]: 99 : if (!q.exec())
560 : 2 : return result;
561 : :
562 [ + - + + ]: 300 : while (q.next()) {
563 : 203 : CalendarEvent ev;
564 [ + - + - ]: 203 : ev.id = q.value(0).toLongLong();
565 [ + - + - ]: 203 : ev.uid = q.value(1).toString();
566 [ + - + - ]: 203 : ev.calendarPath = q.value(2).toString();
567 [ + - + - ]: 203 : ev.accountId = q.value(3).toString();
568 [ + - + - ]: 203 : ev.summary = q.value(4).toString();
569 [ + - + - ]: 203 : ev.description = q.value(5).toString();
570 [ + - + - ]: 203 : ev.location = q.value(6).toString();
571 [ + - + - : 203 : if (!q.value(7).isNull())
+ - ]
572 [ + - + - : 203 : ev.dtStart = QDateTime::fromSecsSinceEpoch(q.value(7).toLongLong(), Qt::UTC);
+ - ]
573 [ + - + - : 203 : if (!q.value(8).isNull())
+ + ]
574 [ + - + - : 201 : ev.dtEnd = QDateTime::fromSecsSinceEpoch(q.value(8).toLongLong(), Qt::UTC);
+ - ]
575 [ + - + - ]: 203 : ev.allDay = q.value(9).toBool();
576 [ + - + - ]: 203 : ev.rrule = q.value(10).toString();
577 [ + - + - ]: 203 : ev.etag = q.value(11).toString();
578 [ + - + - : 203 : if (!q.value(12).isNull())
+ + ]
579 : : ev.lastModified =
580 [ + - + - : 3 : QDateTime::fromSecsSinceEpoch(q.value(12).toLongLong(), Qt::UTC);
+ - ]
581 [ + - + - ]: 203 : ev.color = q.value(13).toString();
582 [ + - + - ]: 203 : ev.resourceHref = q.value(14).toString();
583 [ + - ]: 203 : result.append(ev);
584 : 203 : }
585 : 97 : return result;
586 : 99 : }
587 : :
588 : : QList<CalendarEvent>
589 : 14 : CalendarStore::eventsForCalendar(const QString &calendarPath) const {
590 [ + - ]: 14 : return eventsForCalendar(QString(), calendarPath);
591 : : }
592 : :
593 : : QList<CalendarEvent>
594 : 37 : CalendarStore::eventsForCalendar(const QString &accountId,
595 : : const QString &calendarPath) const {
596 : 37 : QList<CalendarEvent> result;
597 [ + - ]: 37 : qint64 calId = calendarIdForPath(accountId, calendarPath);
598 [ + + ]: 37 : if (calId < 0)
599 : 9 : return result;
600 : :
601 [ + - ]: 28 : QSqlQuery q(m_db);
602 [ + - ]: 28 : q.prepare(QStringLiteral(
603 : : "SELECT id, uid, summary, description, location, "
604 : : " dt_start, dt_end, all_day, rrule, etag, last_modified, resource_href "
605 : : "FROM events WHERE calendar_id = ? ORDER BY dt_start ASC"));
606 [ + - ]: 28 : q.addBindValue(calId);
607 [ + - + + ]: 28 : if (!q.exec())
608 : 1 : return result;
609 : :
610 [ + - + + ]: 51 : while (q.next()) {
611 : 24 : CalendarEvent ev;
612 [ + - + - ]: 24 : ev.id = q.value(0).toLongLong();
613 [ + - + - ]: 24 : ev.uid = q.value(1).toString();
614 : 24 : ev.calendarPath = calendarPath;
615 : 24 : ev.accountId = accountId;
616 [ + - + - ]: 24 : ev.summary = q.value(2).toString();
617 [ + - + - ]: 24 : ev.description = q.value(3).toString();
618 [ + - + - ]: 24 : ev.location = q.value(4).toString();
619 [ + - + - : 24 : if (!q.value(5).isNull())
+ + ]
620 [ + - + - : 21 : ev.dtStart = QDateTime::fromSecsSinceEpoch(q.value(5).toLongLong(), Qt::UTC);
+ - ]
621 [ + - + - : 24 : if (!q.value(6).isNull())
+ + ]
622 [ + - + - : 13 : ev.dtEnd = QDateTime::fromSecsSinceEpoch(q.value(6).toLongLong(), Qt::UTC);
+ - ]
623 [ + - + - ]: 24 : ev.allDay = q.value(7).toBool();
624 [ + - + - ]: 24 : ev.rrule = q.value(8).toString();
625 [ + - + - ]: 24 : ev.etag = q.value(9).toString();
626 [ + - + - : 24 : if (!q.value(10).isNull())
+ + ]
627 : : ev.lastModified =
628 [ + - + - : 1 : QDateTime::fromSecsSinceEpoch(q.value(10).toLongLong(), Qt::UTC);
+ - ]
629 [ + - + - ]: 24 : ev.resourceHref = q.value(11).toString();
630 [ + - ]: 24 : result.append(ev);
631 : 24 : }
632 : 27 : return result;
633 : 28 : }
634 : :
635 : 1 : void CalendarStore::removeStaleEvents(const QString &calendarPath,
636 : : const QStringList &activeUids) {
637 [ + - ]: 1 : removeStaleEvents(QString(), calendarPath, activeUids);
638 : 1 : }
639 : :
640 : 8 : void CalendarStore::removeStaleEvents(const QString &accountId,
641 : : const QString &calendarPath,
642 : : const QStringList &activeUids) {
643 [ + - ]: 8 : qint64 calId = calendarIdForPath(accountId, calendarPath);
644 [ + + ]: 8 : if (calId < 0)
645 : 3 : return;
646 : :
647 [ + + ]: 7 : if (activeUids.isEmpty()) {
648 [ + - ]: 2 : QSqlQuery q(m_db);
649 [ + - ]: 2 : q.prepare(QStringLiteral("DELETE FROM events WHERE calendar_id = ?"));
650 [ + - ]: 2 : q.addBindValue(calId);
651 [ + - ]: 2 : q.exec();
652 : 2 : return;
653 : 2 : }
654 : :
655 : 5 : QString error;
656 [ + + + + : 10 : if (!DatabaseSync::removeRowsMissingFromUidSet(
- - ]
657 [ + - ]: 5 : m_db,
658 : 10 : QStringLiteral(
659 : : "DELETE FROM events WHERE calendar_id = ? "
660 : : "AND NOT EXISTS (SELECT 1 FROM mailjd_active_sync_uids active "
661 : : "WHERE active.uid = events.uid)"),
662 : : {calId}, activeUids, &error)) {
663 [ + - + - : 2 : qCWarning(lcCalStore) << "removeStaleEvents:" << error;
+ - + - +
+ ]
664 : : }
665 [ + - - - : 10 : }
- - ]
666 : :
667 : 5 : void CalendarStore::deleteEvent(const QString &uid,
668 : : const QString &calendarPath) {
669 [ + - ]: 5 : deleteEvent(uid, QString(), calendarPath);
670 : 5 : }
671 : :
672 : 12 : void CalendarStore::deleteEvent(const QString &uid,
673 : : const QString &accountId,
674 : : const QString &calendarPath) {
675 [ + - ]: 12 : qint64 calId = calendarIdForPath(accountId, calendarPath);
676 [ + + ]: 12 : if (calId < 0) {
677 [ + - + - : 8 : qCWarning(lcCalStore) << "deleteEvent: calendar not found:" << calendarPath;
+ - + - +
+ ]
678 : 4 : return;
679 : : }
680 : :
681 [ + - ]: 8 : QSqlQuery q(m_db);
682 [ + - ]: 8 : q.prepare(
683 : 16 : QStringLiteral("DELETE FROM events WHERE uid = ? AND calendar_id = ?"));
684 [ + - ]: 8 : q.addBindValue(uid);
685 [ + - ]: 8 : q.addBindValue(calId);
686 [ + - + + ]: 8 : if (!q.exec())
687 [ + - + - : 2 : qCWarning(lcCalStore) << "deleteEvent:" << q.lastError().text();
+ - + - +
- + - +
+ ]
688 : : else
689 [ + - + - : 14 : qCDebug(lcCalStore) << "deleteEvent:" << uid << "rows:"
+ - + - +
- + + ]
690 [ + - + - ]: 7 : << q.numRowsAffected();
691 : 8 : }
692 : :
693 : : // ═══════════════════════════════════════════════════════
694 : : // Tasks
695 : : // ═══════════════════════════════════════════════════════
696 : :
697 : 183 : void CalendarStore::upsertTask(const CalendarTask &task) {
698 [ + - ]: 183 : qint64 calId = calendarIdForPath(task.accountId, task.calendarPath);
699 [ + + ]: 183 : if (calId < 0) {
700 [ + - + - : 8 : qCWarning(lcCalStore) << "upsertTask: calendar not found:"
+ - + + ]
701 [ + - + - ]: 4 : << task.accountId << task.calendarPath;
702 : 4 : return;
703 : : }
704 : :
705 [ + - ]: 179 : QSqlQuery q(m_db);
706 [ + - ]: 179 : q.prepare(QStringLiteral(
707 : : "INSERT INTO tasks (calendar_id, uid, summary, description, due, "
708 : : " percent_complete, priority, status, completed_at, etag, last_modified, "
709 : : " dt_start, created, organizer, resource_href) "
710 : : "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
711 : : "ON CONFLICT(calendar_id, uid) DO UPDATE SET "
712 : : " summary = excluded.summary,"
713 : : " description = excluded.description,"
714 : : " due = excluded.due,"
715 : : " percent_complete = excluded.percent_complete,"
716 : : " priority = excluded.priority,"
717 : : " status = excluded.status,"
718 : : " completed_at = excluded.completed_at,"
719 : : " etag = excluded.etag,"
720 : : " last_modified = excluded.last_modified,"
721 : : " dt_start = excluded.dt_start,"
722 : : " created = excluded.created,"
723 : : " organizer = excluded.organizer,"
724 : : " resource_href = excluded.resource_href"));
725 [ + - ]: 179 : q.addBindValue(calId);
726 [ + - ]: 179 : q.addBindValue(task.uid);
727 [ + - ]: 179 : q.addBindValue(task.summary);
728 [ + - ]: 179 : q.addBindValue(task.description);
729 [ + - + + : 179 : q.addBindValue(task.due.isValid() ? task.due.toSecsSinceEpoch()
+ - + - ]
730 : : : QVariant());
731 [ + - ]: 179 : q.addBindValue(task.percentComplete);
732 [ + - ]: 179 : q.addBindValue(task.priority);
733 [ + - ]: 179 : q.addBindValue(task.status);
734 [ + - + - ]: 358 : q.addBindValue(task.completedAt.isValid()
735 [ + + + - ]: 358 : ? task.completedAt.toSecsSinceEpoch()
736 : : : QVariant());
737 [ + - ]: 179 : q.addBindValue(task.etag);
738 [ + - + - ]: 358 : q.addBindValue(task.lastModified.isValid()
739 [ + + + - ]: 358 : ? task.lastModified.toSecsSinceEpoch()
740 : : : QVariant());
741 [ + - + + : 179 : q.addBindValue(task.dtStart.isValid() ? task.dtStart.toSecsSinceEpoch()
+ - + - ]
742 : : : QVariant());
743 [ + - + + : 179 : q.addBindValue(task.created.isValid() ? task.created.toSecsSinceEpoch()
+ - + - ]
744 : : : QVariant());
745 [ + + + - ]: 179 : q.addBindValue(task.organizer.isEmpty() ? QVariant() : task.organizer);
746 [ + - ]: 179 : q.addBindValue(task.resourceHref);
747 [ + - + + ]: 179 : if (!q.exec())
748 [ + - + - : 2 : qCWarning(lcCalStore) << "upsertTask:" << q.lastError().text();
+ - + - +
- + - +
+ ]
749 : 179 : }
750 : :
751 : : // Helper: populate CalendarTask from query result row.
752 : : // Columns: 0=id, 1=uid, 2=c.path, 3=c.accountId, 4=summary, ...
753 : 456 : static CalendarTask taskFromQuery(QSqlQuery &q) {
754 : 456 : CalendarTask task;
755 [ + - + - ]: 456 : task.id = q.value(0).toLongLong();
756 [ + - + - ]: 456 : task.uid = q.value(1).toString();
757 [ + - + - ]: 456 : task.calendarPath = q.value(2).toString();
758 [ + - + - ]: 456 : task.accountId = q.value(3).toString();
759 [ + - + - ]: 456 : task.summary = q.value(4).toString();
760 [ + - + - ]: 456 : task.description = q.value(5).toString();
761 [ + - + - : 456 : if (!q.value(6).isNull())
+ + ]
762 [ + - + - : 409 : task.due = QDateTime::fromSecsSinceEpoch(q.value(6).toLongLong(), Qt::UTC);
+ - ]
763 [ + - + - ]: 456 : task.percentComplete = q.value(7).toInt();
764 [ + - + - ]: 456 : task.priority = q.value(8).toInt();
765 [ + - + - ]: 456 : task.status = q.value(9).toString();
766 [ + - + - : 456 : if (!q.value(10).isNull())
+ + ]
767 : : task.completedAt =
768 [ + - + - : 5 : QDateTime::fromSecsSinceEpoch(q.value(10).toLongLong(), Qt::UTC);
+ - ]
769 [ + - + - ]: 456 : task.etag = q.value(11).toString();
770 [ + - + - : 456 : if (!q.value(12).isNull())
+ + ]
771 : : task.lastModified =
772 [ + - + - : 34 : QDateTime::fromSecsSinceEpoch(q.value(12).toLongLong(), Qt::UTC);
+ - ]
773 [ + - + - : 456 : if (!q.value(13).isNull())
+ + ]
774 : : task.dtStart =
775 [ + - + - : 7 : QDateTime::fromSecsSinceEpoch(q.value(13).toLongLong(), Qt::UTC);
+ - ]
776 [ + - + - : 456 : if (!q.value(14).isNull())
+ + ]
777 : : task.created =
778 [ + - + - : 11 : QDateTime::fromSecsSinceEpoch(q.value(14).toLongLong(), Qt::UTC);
+ - ]
779 [ + - + - ]: 456 : task.organizer = q.value(15).toString();
780 [ + - + - ]: 456 : task.color = q.value(16).toString();
781 [ + - + - ]: 456 : task.calendarDisplayName = q.value(17).toString();
782 [ + - + - ]: 456 : task.resourceHref = q.value(18).toString();
783 : 456 : return task;
784 : 0 : }
785 : :
786 : : static const char *kTaskSelect =
787 : : "SELECT t.id, t.uid, c.path, c.accountId, t.summary, t.description, t.due, "
788 : : " t.percent_complete, t.priority, t.status, t.completed_at, "
789 : : " t.etag, t.last_modified, t.dt_start, t.created, "
790 : : " t.organizer, c.color, c.displayName, t.resource_href "
791 : : "FROM tasks t JOIN calendars c ON t.calendar_id = c.id ";
792 : :
793 : 95 : QList<CalendarTask> CalendarStore::allTasks() const {
794 : 95 : QList<CalendarTask> result;
795 [ + - ]: 95 : QSqlQuery q(m_db);
796 [ + - + - ]: 190 : q.exec(QString::fromLatin1(kTaskSelect) +
797 [ + - ]: 285 : QStringLiteral("ORDER BY t.due ASC NULLS LAST, t.priority DESC"));
798 [ + - + + ]: 496 : while (q.next())
799 [ + - + - ]: 401 : result.append(taskFromQuery(q));
800 : 95 : return result;
801 : 95 : }
802 : :
803 : 10 : QList<CalendarTask> CalendarStore::openTasks() const {
804 : 10 : QList<CalendarTask> result;
805 [ + - ]: 10 : QSqlQuery q(m_db);
806 [ + - + - ]: 20 : q.exec(QString::fromLatin1(kTaskSelect) +
807 [ + - ]: 30 : QStringLiteral(
808 : : "WHERE t.status != 'COMPLETED' AND t.status != 'CANCELLED' "
809 : : "ORDER BY t.due ASC NULLS LAST, t.priority DESC"));
810 [ + - + + ]: 21 : while (q.next())
811 [ + - + - ]: 11 : result.append(taskFromQuery(q));
812 : 10 : return result;
813 : 10 : }
814 : :
815 : : QList<CalendarTask>
816 : 11 : CalendarStore::tasksForCalendar(const QString &calendarPath) const {
817 [ + - ]: 11 : return tasksForCalendar(QString(), calendarPath);
818 : : }
819 : :
820 : : QList<CalendarTask>
821 : 26 : CalendarStore::tasksForCalendar(const QString &accountId,
822 : : const QString &calendarPath) const {
823 : 26 : QList<CalendarTask> result;
824 [ + - ]: 26 : qint64 calId = calendarIdForPath(accountId, calendarPath);
825 [ + + ]: 26 : if (calId < 0)
826 : 5 : return result;
827 : :
828 [ + - ]: 21 : QSqlQuery q(m_db);
829 [ + - + - ]: 42 : q.prepare(QString::fromLatin1(kTaskSelect) +
830 [ + - ]: 63 : QStringLiteral(
831 : : "WHERE t.calendar_id = ? "
832 : : "ORDER BY t.due ASC NULLS LAST, t.priority DESC"));
833 [ + - ]: 21 : q.addBindValue(calId);
834 [ + - + + ]: 21 : if (!q.exec())
835 : 1 : return result;
836 : :
837 [ + - + + ]: 42 : while (q.next())
838 [ + - + - ]: 22 : result.append(taskFromQuery(q));
839 : 20 : return result;
840 : 21 : }
841 : :
842 : 2 : void CalendarStore::removeStaleTasks(const QString &calendarPath,
843 : : const QStringList &activeUids) {
844 [ + - ]: 2 : removeStaleTasks(QString(), calendarPath, activeUids);
845 : 2 : }
846 : :
847 : 10 : void CalendarStore::removeStaleTasks(const QString &accountId,
848 : : const QString &calendarPath,
849 : : const QStringList &activeUids) {
850 [ + - ]: 10 : qint64 calId = calendarIdForPath(accountId, calendarPath);
851 [ + + ]: 10 : if (calId < 0)
852 : 3 : return;
853 : :
854 [ + + ]: 9 : if (activeUids.isEmpty()) {
855 [ + - ]: 2 : QSqlQuery q(m_db);
856 [ + - ]: 2 : q.prepare(QStringLiteral("DELETE FROM tasks WHERE calendar_id = ?"));
857 [ + - ]: 2 : q.addBindValue(calId);
858 [ + - ]: 2 : q.exec();
859 : 2 : return;
860 : 2 : }
861 : :
862 : 7 : QString error;
863 [ + + + + : 14 : if (!DatabaseSync::removeRowsMissingFromUidSet(
- - ]
864 [ + - ]: 7 : m_db,
865 : 14 : QStringLiteral(
866 : : "DELETE FROM tasks WHERE calendar_id = ? "
867 : : "AND NOT EXISTS (SELECT 1 FROM mailjd_active_sync_uids active "
868 : : "WHERE active.uid = tasks.uid)"),
869 : : {calId}, activeUids, &error)) {
870 [ + - + - : 2 : qCWarning(lcCalStore) << "removeStaleTasks:" << error;
+ - + - +
+ ]
871 : : }
872 [ + - - - : 14 : }
- - ]
873 : :
874 : 5 : void CalendarStore::deleteTask(const QString &uid,
875 : : const QString &calendarPath) {
876 [ + - ]: 5 : deleteTask(uid, QString(), calendarPath);
877 : 5 : }
878 : :
879 : 10 : void CalendarStore::deleteTask(const QString &uid,
880 : : const QString &accountId,
881 : : const QString &calendarPath) {
882 [ + - ]: 10 : qint64 calId = calendarIdForPath(accountId, calendarPath);
883 [ + + ]: 10 : if (calId < 0) {
884 [ + - + - : 8 : qCWarning(lcCalStore) << "deleteTask: calendar not found:" << calendarPath;
+ - + - +
+ ]
885 : 4 : return;
886 : : }
887 [ + - ]: 6 : QSqlQuery q(m_db);
888 [ + - ]: 6 : q.prepare(
889 : 12 : QStringLiteral("DELETE FROM tasks WHERE uid = ? AND calendar_id = ?"));
890 [ + - ]: 6 : q.addBindValue(uid);
891 [ + - ]: 6 : q.addBindValue(calId);
892 [ + - + + ]: 6 : if (!q.exec())
893 [ + - + - : 2 : qCWarning(lcCalStore) << "deleteTask:" << q.lastError().text();
+ - + - +
- + - +
+ ]
894 : : else
895 [ + - + - : 10 : qCDebug(lcCalStore) << "deleteTask:" << uid << "rows:"
+ - + - +
- + + ]
896 [ + - + - ]: 5 : << q.numRowsAffected();
897 : 6 : }
898 : :
899 : : // ═══════════════════════════════════════════════════════
900 : : // Task Queries (Sprint 37 – T-451)
901 : : // ═══════════════════════════════════════════════════════
902 : :
903 : 87 : QMap<QString, int> CalendarStore::openTaskCountByCalendar() const {
904 : 87 : QMap<QString, int> result;
905 [ + - ]: 87 : QSqlQuery q(m_db);
906 [ + - ]: 87 : q.exec(QStringLiteral(
907 : : "SELECT c.path, COUNT(*) FROM tasks t "
908 : : "JOIN calendars c ON t.calendar_id = c.id "
909 : : "WHERE t.status != 'COMPLETED' AND t.status != 'CANCELLED' "
910 : : "GROUP BY c.path"));
911 [ + - + + ]: 247 : while (q.next())
912 [ + - + - : 160 : result.insert(q.value(0).toString(), q.value(1).toInt());
+ - + - +
- ]
913 : 87 : return result;
914 : 87 : }
915 : :
916 : : QList<CalendarTask>
917 : 14 : CalendarStore::searchTasks(const QString &query,
918 : : bool includeCompleted) const {
919 : 14 : QList<CalendarTask> result;
920 [ + + ]: 14 : if (query.isEmpty())
921 : 2 : return result;
922 : :
923 : : QString likePattern =
924 [ + - + - ]: 12 : QLatin1Char('%') + query + QLatin1Char('%');
925 : :
926 [ + - ]: 12 : QSqlQuery q(m_db);
927 [ + - ]: 24 : QString sql = QString::fromLatin1(kTaskSelect) +
928 [ + - ]: 36 : QStringLiteral(
929 : : "WHERE (t.summary LIKE ? OR t.description LIKE ?) ");
930 [ + + ]: 12 : if (!includeCompleted)
931 [ + - ]: 9 : sql += QStringLiteral(
932 : : "AND t.status != 'COMPLETED' AND t.status != 'CANCELLED' ");
933 [ + - ]: 12 : sql += QStringLiteral("ORDER BY t.due ASC NULLS LAST, t.priority DESC");
934 : :
935 [ + - ]: 12 : q.prepare(sql);
936 [ + - ]: 12 : q.addBindValue(likePattern);
937 [ + - ]: 12 : q.addBindValue(likePattern);
938 [ + - + + ]: 12 : if (!q.exec())
939 : 1 : return result;
940 : :
941 [ + - + + ]: 21 : while (q.next())
942 [ + - + - ]: 10 : result.append(taskFromQuery(q));
943 : 11 : return result;
944 : 12 : }
945 : :
946 : 6 : QList<CalendarTask> CalendarStore::urgentTasks() const {
947 : 6 : QList<CalendarTask> result;
948 [ + - ]: 6 : QSqlQuery q(m_db);
949 : :
950 : : // Tasks due within 7 days (including overdue)
951 [ + - + - ]: 6 : qint64 now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
952 : 6 : qint64 sevenDays = now + 7 * 24 * 3600;
953 : :
954 [ + - + - ]: 12 : q.prepare(QString::fromLatin1(kTaskSelect) +
955 [ + - ]: 18 : QStringLiteral(
956 : : "WHERE t.due IS NOT NULL AND t.due <= ? "
957 : : "AND t.status != 'COMPLETED' AND t.status != 'CANCELLED' "
958 : : "ORDER BY t.due ASC"));
959 [ + - ]: 6 : q.addBindValue(sevenDays);
960 [ + - + + ]: 6 : if (!q.exec())
961 : 2 : return result;
962 : :
963 [ + - + + ]: 11 : while (q.next())
964 [ + - + - ]: 7 : result.append(taskFromQuery(q));
965 : 4 : return result;
966 : 6 : }
967 : :
968 : 8 : QList<CalendarTask> CalendarStore::completedTasks() const {
969 : 8 : QList<CalendarTask> result;
970 [ + - ]: 8 : QSqlQuery q(m_db);
971 [ + - + - ]: 16 : q.exec(QString::fromLatin1(kTaskSelect) +
972 [ + - ]: 24 : QStringLiteral(
973 : : "WHERE t.status = 'COMPLETED' "
974 : : "ORDER BY t.completed_at DESC NULLS LAST"));
975 [ + - + + ]: 13 : while (q.next())
976 [ + - + - ]: 5 : result.append(taskFromQuery(q));
977 : 8 : return result;
978 : 8 : }
|