MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - data - CalendarStore.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 97.8 % 581 568
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 43 43
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 56.3 % 1560 879

             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 : }
        

Generated by: LCOV version 2.0-1