File indexing completed on 2025-01-05 04:46:56

0001 /***************************************************************************
0002  *   SPDX-FileCopyrightText: 2006 Andreas Gungl <a.gungl@gmx.de>           *
0003  *   SPDX-FileCopyrightText: 2007 Robert Zwerus <arzie@dds.nl>             *
0004  *                                                                         *
0005  *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
0006  ***************************************************************************/
0007 
0008 #include "datastore.h"
0009 
0010 #include "akonadi.h"
0011 #include "akonadischema.h"
0012 #include "akonadiserver_debug.h"
0013 #include "collectionqueryhelper.h"
0014 #include "collectionstatistics.h"
0015 #include "countquerybuilder.h"
0016 #include "dbconfig.h"
0017 #include "dbinitializer.h"
0018 #include "dbupdater.h"
0019 #include "handler.h"
0020 #include "handlerhelper.h"
0021 #include "notificationmanager.h"
0022 #include "parthelper.h"
0023 #include "parttypehelper.h"
0024 #include "querycache.h"
0025 #include "queryhelper.h"
0026 #include "selectquerybuilder.h"
0027 #include "storagedebugger.h"
0028 #include "tracer.h"
0029 #include "transaction.h"
0030 #include "utils.h"
0031 
0032 #include "private/externalpartstorage_p.h"
0033 
0034 #include <QCoreApplication>
0035 #include <QElapsedTimer>
0036 #include <QFile>
0037 #include <QSqlDriver>
0038 #include <QSqlQuery>
0039 #include <QString>
0040 #include <QStringList>
0041 #include <QThread>
0042 #include <QTimer>
0043 #include <QUuid>
0044 #include <QVariant>
0045 
0046 #include <functional>
0047 #include <shared_mutex>
0048 
0049 using namespace Akonadi;
0050 using namespace Akonadi::Server;
0051 
0052 static QThreadStorage<DataStore *> sInstances;
0053 
0054 class DataStoreDbMap
0055 {
0056 public:
0057     void registerDataStore(DataStore *store, const QString &connectionName)
0058     {
0059         std::unique_lock lock{m_mutex};
0060         m_table.insert(connectionName, store);
0061     }
0062 
0063     void unregisterDataStore(const QString &connectionName)
0064     {
0065         std::unique_lock lock{m_mutex};
0066         m_table.remove(connectionName);
0067     }
0068 
0069     DataStore *lookupByConnection(const QSqlDatabase &db)
0070     {
0071         std::shared_lock lock{m_mutex};
0072         auto *store = m_table.value(db.connectionName(), nullptr);
0073         Q_ASSERT(store);
0074         return store;
0075     }
0076 
0077 private:
0078     std::shared_mutex m_mutex;
0079     QHash<QString, DataStore *> m_table;
0080 };
0081 
0082 static DataStoreDbMap sStoreLookup;
0083 
0084 static inline void setBoolPtr(bool *ptr, bool val)
0085 {
0086     if (ptr) {
0087         *ptr = val;
0088     }
0089 }
0090 
0091 std::unique_ptr<DataStoreFactory> DataStore::sFactory;
0092 
0093 void DataStore::setFactory(std::unique_ptr<DataStoreFactory> factory)
0094 {
0095     sFactory = std::move(factory);
0096 }
0097 
0098 DataStore *DataStore::dataStoreForDatabase(const QSqlDatabase &db)
0099 {
0100     return sStoreLookup.lookupByConnection(db);
0101 }
0102 
0103 /***************************************************************************
0104  *   DataStore                                                           *
0105  ***************************************************************************/
0106 DataStore::DataStore(AkonadiServer *akonadi, DbConfig *dbConfig)
0107     : m_akonadi(akonadi)
0108     , m_dbConfig(dbConfig)
0109     , m_dbOpened(false)
0110     , m_transactionLevel(0)
0111     , m_keepAliveTimer(nullptr)
0112 {
0113     if (dbConfig->driverName() == QLatin1StringView("QMYSQL")) {
0114         // Send a dummy query to MySQL every 1 hour to keep the connection alive,
0115         // otherwise MySQL just drops the connection and our subsequent queries fail
0116         // without properly reporting the error
0117         m_keepAliveTimer = new QTimer(this);
0118         m_keepAliveTimer->setInterval(3600 * 1000);
0119         QObject::connect(m_keepAliveTimer, &QTimer::timeout, this, &DataStore::sendKeepAliveQuery);
0120     }
0121 }
0122 
0123 DataStore::DataStore(DbConfig *dbConfig)
0124     : DataStore(nullptr, dbConfig)
0125 {
0126 }
0127 
0128 DataStore::~DataStore()
0129 {
0130     Q_ASSERT_X(!m_dbOpened, "DataStore", "Attempting to destroy DataStore with opened DB connection. Call close() first!");
0131 }
0132 
0133 void DataStore::open()
0134 {
0135     m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast<qulonglong>(QThread::currentThread()));
0136     Q_ASSERT(!QSqlDatabase::contains(m_connectionName));
0137 
0138     m_database = QSqlDatabase::addDatabase(m_dbConfig->driverName(), m_connectionName);
0139     sStoreLookup.registerDataStore(this, m_connectionName);
0140     m_dbConfig->apply(m_database);
0141 
0142     if (!m_database.isValid()) {
0143         m_dbOpened = false;
0144         return;
0145     }
0146     m_dbOpened = m_database.open();
0147 
0148     if (!m_dbOpened) {
0149         qCCritical(AKONADISERVER_LOG) << "Database error: Cannot open database.";
0150         qCCritical(AKONADISERVER_LOG) << "  Last driver error:" << m_database.lastError().driverText();
0151         qCCritical(AKONADISERVER_LOG) << "  Last database error:" << m_database.lastError().databaseText();
0152         return;
0153     } else {
0154         qCDebug(AKONADISERVER_LOG) << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName();
0155     }
0156 
0157     StorageDebugger::instance()->addConnection(reinterpret_cast<qint64>(this), QThread::currentThread()->objectName());
0158     connect(QThread::currentThread(), &QThread::objectNameChanged, this, [this](const QString &name) {
0159         if (!name.isEmpty()) {
0160             StorageDebugger::instance()->changeConnection(reinterpret_cast<qint64>(this), name);
0161         }
0162     });
0163 
0164     m_dbConfig->initSession(m_database);
0165 
0166     if (m_keepAliveTimer) {
0167         m_keepAliveTimer->start();
0168     }
0169 }
0170 
0171 QSqlDatabase DataStore::database()
0172 {
0173     if (!m_dbOpened) {
0174         open();
0175     }
0176     return m_database;
0177 }
0178 
0179 void DataStore::close()
0180 {
0181     if (m_keepAliveTimer) {
0182         m_keepAliveTimer->stop();
0183     }
0184 
0185     if (!m_dbOpened) {
0186         return;
0187     }
0188 
0189     if (inTransaction()) {
0190         // By setting m_transactionLevel to '1' here, we skip all nested transactions
0191         // and rollback the outermost transaction.
0192         m_transactionLevel = 1;
0193         rollbackTransaction();
0194     }
0195 
0196     QueryCache::clear();
0197     m_database.close();
0198     m_database = QSqlDatabase();
0199     QSqlDatabase::removeDatabase(m_connectionName);
0200     sStoreLookup.unregisterDataStore(m_connectionName);
0201 
0202     StorageDebugger::instance()->removeConnection(reinterpret_cast<qint64>(this));
0203 
0204     m_dbOpened = false;
0205 }
0206 
0207 bool DataStore::init()
0208 {
0209     // Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread());
0210 
0211     AkonadiSchema schema;
0212     DbInitializer::Ptr initializer = DbInitializer::createInstance(m_database, &schema);
0213     if (!initializer->run()) {
0214         qCCritical(AKONADISERVER_LOG) << initializer->errorMsg();
0215         return false;
0216     }
0217 
0218     if (QFile::exists(QStringLiteral(":dbupdate.xml"))) {
0219         DbUpdater updater(m_database, QStringLiteral(":dbupdate.xml"));
0220         if (!updater.run()) {
0221             return false;
0222         }
0223     } else {
0224         qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates";
0225     }
0226 
0227     if (!initializer->updateIndexesAndConstraints()) {
0228         qCCritical(AKONADISERVER_LOG) << initializer->errorMsg();
0229         return false;
0230     }
0231 
0232     // enable caching for some tables
0233     MimeType::enableCache(true);
0234     Flag::enableCache(true);
0235     Resource::enableCache(true);
0236     Collection::enableCache(true);
0237     PartType::enableCache(true);
0238 
0239     return true;
0240 }
0241 
0242 NotificationCollector *DataStore::notificationCollector()
0243 {
0244     Q_ASSERT(m_akonadi);
0245     if (!mNotificationCollector) {
0246         mNotificationCollector = std::make_unique<NotificationCollector>(*m_akonadi, this);
0247     }
0248 
0249     return mNotificationCollector.get();
0250 }
0251 
0252 DataStore *DataStore::self()
0253 {
0254     if (!sInstances.hasLocalData()) {
0255         sInstances.setLocalData(sFactory->createStore());
0256     }
0257     return sInstances.localData();
0258 }
0259 
0260 bool DataStore::hasDataStore()
0261 {
0262     return sInstances.hasLocalData();
0263 }
0264 
0265 /* --- ItemFlags ----------------------------------------------------- */
0266 
0267 bool DataStore::setItemsFlags(const PimItem::List &items,
0268                               const QList<Flag> *currentFlags,
0269                               const QList<Flag> &newFlags,
0270                               bool *flagsChanged,
0271                               const Collection &col_,
0272                               bool silent)
0273 {
0274     QSet<QString> removedFlags;
0275     QSet<QString> addedFlags;
0276     QVariantList insIds;
0277     QVariantList insFlags;
0278     Query::Condition delConds(Query::Or);
0279     Collection col = col_;
0280 
0281     setBoolPtr(flagsChanged, false);
0282 
0283     for (const PimItem &item : items) {
0284         const Flag::List itemFlags = currentFlags ? *currentFlags : item.flags(); // optimization
0285         for (const Flag &flag : itemFlags) {
0286             if (!newFlags.contains(flag)) {
0287                 removedFlags << flag.name();
0288                 Query::Condition cond;
0289                 cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id());
0290                 cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id());
0291                 delConds.addCondition(cond);
0292             }
0293         }
0294 
0295         for (const Flag &flag : newFlags) {
0296             if (!itemFlags.contains(flag)) {
0297                 addedFlags << flag.name();
0298                 insIds << item.id();
0299                 insFlags << flag.id();
0300             }
0301         }
0302 
0303         if (col.id() == -1) {
0304             col.setId(item.collectionId());
0305         } else if (col.id() != item.collectionId()) {
0306             col.setId(-2);
0307         }
0308     }
0309 
0310     if (!removedFlags.empty()) {
0311         QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete);
0312         qb.addCondition(delConds);
0313         if (!qb.exec()) {
0314             return false;
0315         }
0316     }
0317 
0318     if (!addedFlags.empty()) {
0319         QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert);
0320         qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds);
0321         qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags);
0322         qb2.setIdentificationColumn(QString());
0323         if (!qb2.exec()) {
0324             return false;
0325         }
0326     }
0327 
0328     if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) {
0329         QSet<QByteArray> addedFlagsBa;
0330         QSet<QByteArray> removedFlagsBa;
0331         for (const auto &addedFlag : std::as_const(addedFlags)) {
0332             addedFlagsBa.insert(addedFlag.toLatin1());
0333         }
0334         for (const auto &removedFlag : std::as_const(removedFlags)) {
0335             removedFlagsBa.insert(removedFlag.toLatin1());
0336         }
0337         notificationCollector()->itemsFlagsChanged(items, addedFlagsBa, removedFlagsBa, col);
0338     }
0339 
0340     setBoolPtr(flagsChanged, (addedFlags != removedFlags));
0341 
0342     return true;
0343 }
0344 
0345 bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet<Entity::Id> &existing, const Collection &col_, bool silent)
0346 {
0347     Collection col = col_;
0348     QVariantList flagIds;
0349     QVariantList appendIds;
0350     PimItem::List appendItems;
0351     for (const PimItem &item : items) {
0352         if (existing.contains(item.id())) {
0353             continue;
0354         }
0355 
0356         flagIds << flag.id();
0357         appendIds << item.id();
0358         appendItems << item;
0359 
0360         if (col.id() == -1) {
0361             col.setId(item.collectionId());
0362         } else if (col.id() != item.collectionId()) {
0363             col.setId(-2);
0364         }
0365     }
0366 
0367     if (appendItems.isEmpty()) {
0368         return true; // all items have the desired flags already
0369     }
0370 
0371     QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert);
0372     qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds);
0373     qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds);
0374     qb2.setIdentificationColumn(QString());
0375     if (!qb2.exec()) {
0376         qCWarning(AKONADISERVER_LOG) << "Failed to append flag" << flag.name() << "to Items" << appendIds;
0377         return false;
0378     }
0379 
0380     if (!silent) {
0381         notificationCollector()->itemsFlagsChanged(appendItems, {flag.name().toLatin1()}, {}, col);
0382     }
0383 
0384     return true;
0385 }
0386 
0387 bool DataStore::appendItemsFlags(const PimItem::List &items,
0388                                  const QList<Flag> &flags,
0389                                  bool *flagsChanged,
0390                                  bool checkIfExists,
0391                                  const Collection &col,
0392                                  bool silent)
0393 {
0394     QVariantList itemsIds;
0395     itemsIds.reserve(items.count());
0396     for (const PimItem &item : items) {
0397         itemsIds.append(item.id());
0398     }
0399 
0400     setBoolPtr(flagsChanged, false);
0401 
0402     for (const Flag &flag : flags) {
0403         QSet<PimItem::Id> existing;
0404         if (checkIfExists) {
0405             QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select);
0406             Query::Condition cond;
0407             cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id());
0408             cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds);
0409             qb.addColumn(PimItemFlagRelation::leftColumn());
0410             qb.addCondition(cond);
0411 
0412             if (!qb.exec()) {
0413                 qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing flags for Items " << itemsIds;
0414                 return false;
0415             }
0416 
0417             QSqlQuery query = qb.query();
0418             if (query.driver()->hasFeature(QSqlDriver::QuerySize)) {
0419                 // The query size feature is not supported by the sqllite driver
0420                 if (query.size() == items.count()) {
0421                     continue;
0422                 }
0423                 setBoolPtr(flagsChanged, true);
0424             }
0425 
0426             while (query.next()) {
0427                 existing << query.value(0).value<PimItem::Id>();
0428             }
0429             if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) {
0430                 if (existing.size() != items.count()) {
0431                     setBoolPtr(flagsChanged, true);
0432                 }
0433             }
0434             query.finish();
0435         }
0436 
0437         if (!doAppendItemsFlag(items, flag, existing, col, silent)) {
0438             return false;
0439         }
0440     }
0441 
0442     return true;
0443 }
0444 
0445 bool DataStore::removeItemsFlags(const PimItem::List &items, const QList<Flag> &flags, bool *flagsChanged, const Collection &col_, bool silent)
0446 {
0447     Collection col = col_;
0448     QSet<QString> removedFlags;
0449     QVariantList itemsIds;
0450     QVariantList flagsIds;
0451 
0452     setBoolPtr(flagsChanged, false);
0453     itemsIds.reserve(items.count());
0454 
0455     for (const PimItem &item : items) {
0456         itemsIds << item.id();
0457         if (col.id() == -1) {
0458             col.setId(item.collectionId());
0459         } else if (col.id() != item.collectionId()) {
0460             col.setId(-2);
0461         }
0462         for (int i = 0; i < flags.count(); ++i) {
0463             const QString flagName = flags[i].name();
0464             if (!removedFlags.contains(flagName)) {
0465                 flagsIds << flags[i].id();
0466                 removedFlags << flagName;
0467             }
0468         }
0469     }
0470 
0471     // Delete all given flags from all given items in one go
0472     QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete);
0473     Query::Condition cond(Query::And);
0474     cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds);
0475     cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds);
0476     qb.addCondition(cond);
0477     if (!qb.exec()) {
0478         qCWarning(AKONADISERVER_LOG) << "Failed to remove flags" << flags << "from Items" << itemsIds;
0479         return false;
0480     }
0481 
0482     if (qb.query().numRowsAffected() != 0) {
0483         setBoolPtr(flagsChanged, true);
0484         if (!silent) {
0485             QSet<QByteArray> removedFlagsBa;
0486             for (const auto &remoteFlag : std::as_const(removedFlags)) {
0487                 removedFlagsBa.insert(remoteFlag.toLatin1());
0488             }
0489             notificationCollector()->itemsFlagsChanged(items, {}, removedFlagsBa, col);
0490         }
0491     }
0492 
0493     return true;
0494 }
0495 
0496 /* --- ItemTags ----------------------------------------------------- */
0497 
0498 bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent)
0499 {
0500     QSet<qint64> removedTags;
0501     QSet<qint64> addedTags;
0502     QVariantList insIds;
0503     QVariantList insTags;
0504     Query::Condition delConds(Query::Or);
0505 
0506     setBoolPtr(tagsChanged, false);
0507 
0508     for (const PimItem &item : items) {
0509         const Tag::List itemTags = item.tags();
0510         for (const Tag &tag : itemTags) {
0511             if (!tags.contains(tag)) {
0512                 // Remove tags from items that had it set
0513                 removedTags << tag.id();
0514                 Query::Condition cond;
0515                 cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id());
0516                 cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id());
0517                 delConds.addCondition(cond);
0518             }
0519         }
0520 
0521         for (const Tag &tag : tags) {
0522             if (!itemTags.contains(tag)) {
0523                 // Add tags to items that did not have the tag
0524                 addedTags << tag.id();
0525                 insIds << item.id();
0526                 insTags << tag.id();
0527             }
0528         }
0529     }
0530 
0531     if (!removedTags.empty()) {
0532         QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete);
0533         qb.addCondition(delConds);
0534         if (!qb.exec()) {
0535             qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTags << "from Items";
0536             return false;
0537         }
0538     }
0539 
0540     if (!addedTags.empty()) {
0541         QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert);
0542         qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds);
0543         qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags);
0544         qb2.setIdentificationColumn(QString());
0545         if (!qb2.exec()) {
0546             qCWarning(AKONADISERVER_LOG) << "Failed to add tags" << addedTags << "to Items";
0547             return false;
0548         }
0549     }
0550 
0551     if (!silent && (!addedTags.empty() || !removedTags.empty())) {
0552         notificationCollector()->itemsTagsChanged(items, addedTags, removedTags);
0553     }
0554 
0555     setBoolPtr(tagsChanged, (addedTags != removedTags));
0556 
0557     return true;
0558 }
0559 
0560 bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet<Entity::Id> &existing, const Collection &col, bool silent)
0561 {
0562     QVariantList tagIds;
0563     QVariantList appendIds;
0564     PimItem::List appendItems;
0565     for (const PimItem &item : items) {
0566         if (existing.contains(item.id())) {
0567             continue;
0568         }
0569 
0570         tagIds << tag.id();
0571         appendIds << item.id();
0572         appendItems << item;
0573     }
0574 
0575     if (appendItems.isEmpty()) {
0576         return true; // all items have the desired tags already
0577     }
0578 
0579     QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert);
0580     qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds);
0581     qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds);
0582     qb2.setIdentificationColumn(QString());
0583     if (!qb2.exec()) {
0584         qCWarning(AKONADISERVER_LOG) << "Failed to append tag" << tag << "to Items" << appendItems;
0585         return false;
0586     }
0587 
0588     if (!silent) {
0589         notificationCollector()->itemsTagsChanged(appendItems, {tag.id()}, {}, col);
0590     }
0591 
0592     return true;
0593 }
0594 
0595 bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool checkIfExists, const Collection &col, bool silent)
0596 {
0597     QVariantList itemsIds;
0598     itemsIds.reserve(items.count());
0599     for (const PimItem &item : items) {
0600         itemsIds.append(item.id());
0601     }
0602 
0603     setBoolPtr(tagsChanged, false);
0604 
0605     for (const Tag &tag : tags) {
0606         QSet<PimItem::Id> existing;
0607         if (checkIfExists) {
0608             QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select);
0609             Query::Condition cond;
0610             cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id());
0611             cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds);
0612             qb.addColumn(PimItemTagRelation::leftColumn());
0613             qb.addCondition(cond);
0614 
0615             if (!qb.exec()) {
0616                 qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing tag" << tag << "for Items" << itemsIds;
0617                 return false;
0618             }
0619 
0620             QSqlQuery query = qb.query();
0621             if (query.driver()->hasFeature(QSqlDriver::QuerySize)) {
0622                 if (query.size() == items.count()) {
0623                     continue;
0624                 }
0625                 setBoolPtr(tagsChanged, true);
0626             }
0627 
0628             while (query.next()) {
0629                 existing << query.value(0).value<PimItem::Id>();
0630             }
0631             if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) {
0632                 if (existing.size() != items.count()) {
0633                     setBoolPtr(tagsChanged, true);
0634                 }
0635             }
0636             query.finish();
0637         }
0638 
0639         if (!doAppendItemsTag(items, tag, existing, col, silent)) {
0640             return false;
0641         }
0642     }
0643 
0644     return true;
0645 }
0646 
0647 bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent)
0648 {
0649     QSet<qint64> removedTags;
0650     QVariantList itemsIds;
0651     QVariantList tagsIds;
0652 
0653     setBoolPtr(tagsChanged, false);
0654     itemsIds.reserve(items.count());
0655 
0656     for (const PimItem &item : items) {
0657         itemsIds << item.id();
0658         for (int i = 0; i < tags.count(); ++i) {
0659             const qint64 tagId = tags[i].id();
0660             if (!removedTags.contains(tagId)) {
0661                 tagsIds << tagId;
0662                 removedTags << tagId;
0663             }
0664         }
0665     }
0666 
0667     // Delete all given tags from all given items in one go
0668     QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete);
0669     Query::Condition cond(Query::And);
0670     cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds);
0671     cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds);
0672     qb.addCondition(cond);
0673     if (!qb.exec()) {
0674         qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << tagsIds << "from Items" << itemsIds;
0675         return false;
0676     }
0677 
0678     if (qb.query().numRowsAffected() != 0) {
0679         setBoolPtr(tagsChanged, true);
0680         if (!silent) {
0681             notificationCollector()->itemsTagsChanged(items, QSet<qint64>(), removedTags);
0682         }
0683     }
0684 
0685     return true;
0686 }
0687 
0688 bool DataStore::removeTags(const Tag::List &tags, bool silent)
0689 {
0690     // Currently the "silent" argument is only for API symmetry
0691     Q_UNUSED(silent)
0692 
0693     QVariantList removedTagsIds;
0694     QSet<qint64> removedTags;
0695     removedTagsIds.reserve(tags.count());
0696     removedTags.reserve(tags.count());
0697     for (const Tag &tag : tags) {
0698         removedTagsIds << tag.id();
0699         removedTags << tag.id();
0700     }
0701 
0702     // Get all PIM items that we will untag
0703     SelectQueryBuilder<PimItem> itemsQuery;
0704     itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName());
0705     itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds);
0706 
0707     if (!itemsQuery.exec()) {
0708         qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to query Items for given tags" << removedTagsIds;
0709         return false;
0710     }
0711     const PimItem::List items = itemsQuery.result();
0712 
0713     if (!items.isEmpty()) {
0714         notificationCollector()->itemsTagsChanged(items, QSet<qint64>(), removedTags);
0715     }
0716 
0717     for (const Tag &tag : tags) {
0718         // Emit special tagRemoved notification for each resource that owns the tag
0719         QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select);
0720         qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName());
0721         qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName());
0722         qb.addColumn(Resource::nameFullColumnName());
0723         qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id());
0724         if (!qb.exec()) {
0725             qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to retrieve RIDs for tag" << tag.id();
0726             return false;
0727         }
0728 
0729         // Emit specialized notifications for each resource
0730         QSqlQuery query = qb.query();
0731         while (query.next()) {
0732             const QString rid = query.value(0).toString();
0733             const QByteArray resource = query.value(1).toByteArray();
0734 
0735             notificationCollector()->tagRemoved(tag, resource, rid);
0736         }
0737         query.finish();
0738 
0739         // And one for clients - without RID
0740         notificationCollector()->tagRemoved(tag, QByteArray(), QString());
0741     }
0742 
0743     // Just remove the tags, table constraints will take care of the rest
0744     QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete);
0745     qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds);
0746     if (!qb.exec()) {
0747         qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTagsIds;
0748         return false;
0749     }
0750 
0751     return true;
0752 }
0753 
0754 /* --- ItemParts ----------------------------------------------------- */
0755 
0756 bool DataStore::removeItemParts(const PimItem &item, const QSet<QByteArray> &parts)
0757 {
0758     SelectQueryBuilder<Part> qb;
0759     qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName());
0760     qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id());
0761     qb.addCondition(PartTypeHelper::conditionFromFqNames(parts));
0762 
0763     if (!qb.exec()) {
0764         qCWarning(AKONADISERVER_LOG) << "Removing item parts failed: failed to query parts" << parts << "from Item " << item.id();
0765         return false;
0766     }
0767 
0768     const Part::List existingParts = qb.result();
0769     for (Part part : std::as_const(existingParts)) {
0770         if (!PartHelper::remove(&part)) {
0771             qCWarning(AKONADISERVER_LOG) << "Failed to remove part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name()
0772                                          << ") from Item" << item.id();
0773             return false;
0774         }
0775     }
0776 
0777     notificationCollector()->itemChanged(item, parts);
0778     return true;
0779 }
0780 
0781 bool DataStore::invalidateItemCache(const PimItem &item)
0782 {
0783     // find all payload item parts
0784     SelectQueryBuilder<Part> qb;
0785     qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName());
0786     qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName());
0787     qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id());
0788     qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
0789     qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1StringView("PLD"));
0790     qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false);
0791 
0792     if (!qb.exec()) {
0793         qCWarning(AKONADISERVER_LOG) << "Failed to invalidate cache for Item" << item.id();
0794         return false;
0795     }
0796 
0797     const Part::List parts = qb.result();
0798     // clear data field
0799     for (Part part : parts) {
0800         if (!PartHelper::truncate(part)) {
0801             qCWarning(AKONADISERVER_LOG) << "Failed to truncate payload part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name()
0802                                          << ") of Item" << item.id();
0803             return false;
0804         }
0805     }
0806 
0807     return true;
0808 }
0809 
0810 /* --- Collection ------------------------------------------------------ */
0811 bool DataStore::appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap<QByteArray, QByteArray> &attributes)
0812 {
0813     // no need to check for already existing collection with the same name,
0814     // a unique index on parent + name prevents that in the database
0815     if (!collection.insert()) {
0816         qCWarning(AKONADISERVER_LOG) << "Failed to append Collection" << collection.name() << "in resource" << collection.resource().name();
0817         return false;
0818     }
0819 
0820     if (!appendMimeTypeForCollection(collection.id(), mimeTypes)) {
0821         qCWarning(AKONADISERVER_LOG) << "Failed to append mimetypes" << mimeTypes << "to new collection" << collection.name() << "(ID" << collection.id()
0822                                      << ") in resource" << collection.resource().name();
0823         return false;
0824     }
0825 
0826     for (auto it = attributes.cbegin(), end = attributes.cend(); it != end; ++it) {
0827         if (!addCollectionAttribute(collection, it.key(), it.value(), true)) {
0828             qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << it.key() << "to new collection" << collection.name() << "(ID" << collection.id()
0829                                          << ") in resource" << collection.resource().name();
0830             return false;
0831         }
0832     }
0833 
0834     notificationCollector()->collectionAdded(collection);
0835     return true;
0836 }
0837 
0838 bool DataStore::cleanupCollection(Collection &collection)
0839 {
0840     // collect item deletion notifications
0841     const PimItem::List items = collection.items();
0842     const QByteArray resource = collection.resource().name().toLatin1();
0843 
0844     // generate the notification before actually removing the data
0845     // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though
0846     notificationCollector()->itemsRemoved(items, collection, resource);
0847 
0848     // remove all external payload parts
0849     QueryBuilder qb(Part::tableName(), QueryBuilder::Select);
0850     qb.addColumn(Part::dataFullColumnName());
0851     qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName());
0852     qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName());
0853     qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id());
0854     qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External);
0855     qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
0856     if (!qb.exec()) {
0857         qCWarning(AKONADISERVER_LOG) << "Failed to cleanup collection" << collection.name() << "(ID" << collection.id() << "):"
0858                                      << "Failed to query existing payload parts";
0859         return false;
0860     }
0861 
0862     try {
0863         while (qb.query().next()) {
0864             ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray()));
0865         }
0866     } catch (const PartHelperException &e) {
0867         qb.query().finish();
0868         qCWarning(AKONADISERVER_LOG) << "PartHelperException while cleaning up collection" << collection.name() << "(ID" << collection.id() << "):" << e.what();
0869         return false;
0870     }
0871     qb.query().finish();
0872 
0873     // delete the collection itself, referential actions will do the rest
0874     notificationCollector()->collectionRemoved(collection);
0875     return collection.remove();
0876 }
0877 
0878 static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId)
0879 {
0880     Transaction transaction(DataStore::self(), QStringLiteral("RECURSIVE SET RESOURCEID"));
0881 
0882     QueryBuilder qb(Collection::tableName(), QueryBuilder::Update);
0883     qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id());
0884     qb.setColumnValue(Collection::resourceIdColumn(), resourceId);
0885     qb.setColumnValue(Collection::remoteIdColumn(), QVariant());
0886     qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant());
0887     if (!qb.exec()) {
0888         qCWarning(AKONADISERVER_LOG) << "Failed to set resource ID" << resourceId << "to collection" << collection.name() << "(ID" << collection.id() << ")";
0889         return false;
0890     }
0891 
0892     // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc)
0893     // as well as mark the items dirty to prevent cache purging before they have been written back
0894     qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update);
0895     qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id());
0896     qb.setColumnValue(PimItem::remoteIdColumn(), QVariant());
0897     qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant());
0898     const QDateTime now = QDateTime::currentDateTimeUtc();
0899     qb.setColumnValue(PimItem::datetimeColumn(), now);
0900     qb.setColumnValue(PimItem::atimeColumn(), now);
0901     qb.setColumnValue(PimItem::dirtyColumn(), true);
0902     if (!qb.exec()) {
0903         qCWarning(AKONADISERVER_LOG) << "Failed reset RID/RREV for PimItems in Collection" << collection.name() << "(ID" << collection.id() << ")";
0904         return false;
0905     }
0906 
0907     transaction.commit();
0908 
0909     const auto children = collection.children();
0910     for (const Collection &col : children) {
0911         if (!recursiveSetResourceId(col, resourceId)) {
0912             return false;
0913         }
0914     }
0915     return true;
0916 }
0917 
0918 bool DataStore::moveCollection(Collection &collection, const Collection &newParent)
0919 {
0920     if (collection.parentId() == newParent.id()) {
0921         return true;
0922     }
0923 
0924     if (!m_dbOpened) {
0925         return false;
0926     }
0927 
0928     if (!newParent.isValid()) {
0929         qCWarning(AKONADISERVER_LOG) << "Failed to move collection" << collection.name() << "(ID" << collection.id() << "): invalid destination";
0930         return false;
0931     }
0932 
0933     const QByteArray oldResource = collection.resource().name().toLatin1();
0934 
0935     int resourceId = collection.resourceId();
0936     const Collection source = collection.parent();
0937     if (newParent.id() > 0) { // not root
0938         resourceId = newParent.resourceId();
0939     }
0940     if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) {
0941         return false;
0942     }
0943 
0944     collection.setParentId(newParent.id());
0945     if (collection.resourceId() != resourceId) {
0946         collection.setResourceId(resourceId);
0947         collection.setRemoteId(QString());
0948         collection.setRemoteRevision(QString());
0949         if (!recursiveSetResourceId(collection, resourceId)) {
0950             return false;
0951         }
0952     }
0953 
0954     if (!collection.update()) {
0955         qCWarning(AKONADISERVER_LOG) << "Failed to move Collection" << collection.name() << "(ID" << collection.id() << ")"
0956                                      << "into Collection" << collection.name() << "(ID" << collection.id() << ")";
0957         return false;
0958     }
0959 
0960     notificationCollector()->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1());
0961     return true;
0962 }
0963 
0964 bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes)
0965 {
0966     if (mimeTypes.isEmpty()) {
0967         return true;
0968     }
0969 
0970     for (const QString &mimeType : mimeTypes) {
0971         const auto &mt = MimeType::retrieveByNameOrCreate(mimeType);
0972         if (!mt.isValid()) {
0973             return false;
0974         }
0975         if (!Collection::addMimeType(collectionId, mt.id())) {
0976             qCWarning(AKONADISERVER_LOG) << "Failed to append mimetype" << mt.name() << "to Collection" << collectionId;
0977             return false;
0978         }
0979     }
0980 
0981     return true;
0982 }
0983 
0984 void DataStore::activeCachePolicy(Collection &col)
0985 {
0986     if (!col.cachePolicyInherit()) {
0987         return;
0988     }
0989 
0990     Collection parent = col;
0991     while (parent.parentId() != 0) {
0992         parent = parent.parent();
0993         if (!parent.cachePolicyInherit()) {
0994             col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval());
0995             col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout());
0996             col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand());
0997             col.setCachePolicyLocalParts(parent.cachePolicyLocalParts());
0998             return;
0999         }
1000     }
1001 
1002     // ### system default
1003     col.setCachePolicyCheckInterval(-1);
1004     col.setCachePolicyCacheTimeout(-1);
1005     col.setCachePolicySyncOnDemand(false);
1006     col.setCachePolicyLocalParts(QStringLiteral("ALL"));
1007 }
1008 
1009 QList<Collection> DataStore::virtualCollections(const PimItem &item)
1010 {
1011     SelectQueryBuilder<Collection> qb;
1012     qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName());
1013     qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id());
1014 
1015     if (!qb.exec()) {
1016         qCWarning(AKONADISERVER_LOG) << "Failed to query virtual collections which PimItem" << item.id() << "belongs into";
1017         return QList<Collection>();
1018     }
1019 
1020     return qb.result();
1021 }
1022 
1023 QMap<Entity::Id, QList<PimItem>> DataStore::virtualCollections(const PimItem::List &items)
1024 {
1025     QueryBuilder qb(CollectionPimItemRelation::tableName(), QueryBuilder::Select);
1026     qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName());
1027     qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), CollectionPimItemRelation::rightFullColumnName());
1028     qb.addColumn(Collection::idFullColumnName());
1029     qb.addColumns(QStringList() << PimItem::idFullColumnName() << PimItem::remoteIdFullColumnName() << PimItem::remoteRevisionFullColumnName()
1030                                 << PimItem::mimeTypeIdFullColumnName());
1031     qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending);
1032 
1033     if (items.count() == 1) {
1034         qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, items.first().id());
1035     } else {
1036         QVariantList ids;
1037         ids.reserve(items.count());
1038         for (const PimItem &item : items) {
1039             ids << item.id();
1040         }
1041         qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::In, ids);
1042     }
1043 
1044     if (!qb.exec()) {
1045         qCWarning(AKONADISERVER_LOG) << "Failed to query virtual Collections which PimItems" << items << "belong into";
1046         return QMap<Entity::Id, QList<PimItem>>();
1047     }
1048 
1049     QSqlQuery query = qb.query();
1050     QMap<Entity::Id, QList<PimItem>> map;
1051     query.next();
1052     while (query.isValid()) {
1053         const qlonglong collectionId = query.value(0).toLongLong();
1054         QList<PimItem> &pimItems = map[collectionId];
1055         do {
1056             PimItem item;
1057             item.setId(query.value(1).toLongLong());
1058             item.setRemoteId(query.value(2).toString());
1059             item.setRemoteRevision(query.value(3).toString());
1060             item.setMimeTypeId(query.value(4).toLongLong());
1061             pimItems << item;
1062         } while (query.next() && query.value(0).toLongLong() == collectionId);
1063     }
1064     query.finish();
1065 
1066     return map;
1067 }
1068 
1069 /* --- PimItem ------------------------------------------------------- */
1070 bool DataStore::appendPimItem(QList<Part> &parts,
1071                               const QList<Flag> &flags,
1072                               const MimeType &mimetype,
1073                               const Collection &collection,
1074                               const QDateTime &dateTime,
1075                               const QString &remote_id,
1076                               const QString &remoteRevision,
1077                               const QString &gid,
1078                               PimItem &pimItem)
1079 {
1080     pimItem.setMimeTypeId(mimetype.id());
1081     pimItem.setCollectionId(collection.id());
1082     if (dateTime.isValid()) {
1083         pimItem.setDatetime(dateTime);
1084     }
1085     if (remote_id.isEmpty()) {
1086         // from application
1087         pimItem.setDirty(true);
1088     } else {
1089         // from resource
1090         pimItem.setRemoteId(remote_id);
1091         pimItem.setDirty(false);
1092     }
1093     pimItem.setRemoteRevision(remoteRevision);
1094     pimItem.setGid(gid);
1095     pimItem.setAtime(QDateTime::currentDateTimeUtc());
1096 
1097     if (!pimItem.insert()) {
1098         qCWarning(AKONADISERVER_LOG) << "Failed to append new PimItem into Collection" << collection.name() << "(ID" << collection.id() << ")";
1099         return false;
1100     }
1101 
1102     // insert every part
1103     if (!parts.isEmpty()) {
1104         // don't use foreach, the caller depends on knowing the part has changed, see the Append handler
1105         for (QList<Part>::iterator it = parts.begin(); it != parts.end(); ++it) {
1106             (*it).setPimItemId(pimItem.id());
1107             if ((*it).datasize() < (*it).data().size()) {
1108                 (*it).setDatasize((*it).data().size());
1109             }
1110 
1111             //      qCDebug(AKONADISERVER_LOG) << "Insert from DataStore::appendPimItem";
1112             if (!PartHelper::insert(&(*it))) {
1113                 qCWarning(AKONADISERVER_LOG) << "Failed to add part" << it->partType().name() << "to new PimItem" << pimItem.id();
1114                 return false;
1115             }
1116         }
1117     }
1118 
1119     bool seen = false;
1120     for (const Flag &flag : flags) {
1121         seen |= (flag.name() == QLatin1StringView(AKONADI_FLAG_SEEN) || flag.name() == QLatin1StringView(AKONADI_FLAG_IGNORED));
1122         if (!pimItem.addFlag(flag)) {
1123             qCWarning(AKONADISERVER_LOG) << "Failed to add flag" << flag.name() << "to new PimItem" << pimItem.id();
1124             return false;
1125         }
1126     }
1127 
1128     //   qCDebug(AKONADISERVER_LOG) << "appendPimItem: " << pimItem;
1129 
1130     notificationCollector()->itemAdded(pimItem, seen, collection);
1131     return true;
1132 }
1133 
1134 bool DataStore::unhidePimItem(PimItem &pimItem)
1135 {
1136     if (!m_dbOpened) {
1137         return false;
1138     }
1139 
1140     qCDebug(AKONADISERVER_LOG) << "DataStore::unhidePimItem(" << pimItem << ")";
1141 
1142     // FIXME: This is inefficient. Using a bit on the PimItemTable record would probably be some orders of magnitude faster...
1143     return removeItemParts(pimItem, {AKONADI_ATTRIBUTE_HIDDEN});
1144 }
1145 
1146 bool DataStore::unhideAllPimItems()
1147 {
1148     if (!m_dbOpened) {
1149         return false;
1150     }
1151 
1152     qCDebug(AKONADISERVER_LOG) << "DataStore::unhideAllPimItems()";
1153 
1154     try {
1155         return PartHelper::remove(Part::partTypeIdFullColumnName(), PartTypeHelper::fromFqName(QStringLiteral("ATR"), QStringLiteral("HIDDEN")).id());
1156     } catch (...) {
1157     } // we can live with this failing
1158 
1159     return false;
1160 }
1161 
1162 bool DataStore::cleanupPimItems(const PimItem::List &items, bool silent)
1163 {
1164     // generate relation removed notifications
1165     if (!silent) {
1166         for (const PimItem &item : items) {
1167             SelectQueryBuilder<Relation> relationQuery;
1168             relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, item.id());
1169             relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, item.id());
1170             relationQuery.setSubQueryMode(Query::Or);
1171 
1172             if (!relationQuery.exec()) {
1173                 throw HandlerException("Failed to obtain relations");
1174             }
1175             const Relation::List relations = relationQuery.result();
1176             for (const Relation &relation : relations) {
1177                 notificationCollector()->relationRemoved(relation);
1178             }
1179         }
1180 
1181         // generate the notification before actually removing the data
1182         notificationCollector()->itemsRemoved(items);
1183     }
1184 
1185     // FIXME: Create a single query to do this
1186     for (const auto &item : items) {
1187         if (!item.clearFlags()) {
1188             qCWarning(AKONADISERVER_LOG) << "Failed to clean up flags from PimItem" << item.id();
1189             return false;
1190         }
1191         if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) {
1192             qCWarning(AKONADISERVER_LOG) << "Failed to clean up parts from PimItem" << item.id();
1193             return false;
1194         }
1195         if (!PimItem::remove(PimItem::idColumn(), item.id())) {
1196             qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id();
1197             return false;
1198         }
1199 
1200         if (!Entity::clearRelation<CollectionPimItemRelation>(item.id(), Entity::Right)) {
1201             qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id() << "from linked collections";
1202             return false;
1203         }
1204     }
1205 
1206     return true;
1207 }
1208 
1209 bool DataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent)
1210 {
1211     SelectQueryBuilder<CollectionAttribute> qb;
1212     qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id());
1213     qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key);
1214     if (!qb.exec()) {
1215         qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id()
1216                                      << "): Failed to query existing attribute";
1217         return false;
1218     }
1219 
1220     if (!qb.result().isEmpty()) {
1221         qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id()
1222                                      << "): Attribute already exists";
1223         return false;
1224     }
1225 
1226     CollectionAttribute attr;
1227     attr.setCollectionId(col.id());
1228     attr.setType(key);
1229     attr.setValue(value);
1230 
1231     if (!attr.insert()) {
1232         qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() << ")";
1233         return false;
1234     }
1235 
1236     if (!silent) {
1237         notificationCollector()->collectionChanged(col, QList<QByteArray>() << key);
1238     }
1239     return true;
1240 }
1241 
1242 bool DataStore::removeCollectionAttribute(const Collection &col, const QByteArray &key)
1243 {
1244     SelectQueryBuilder<CollectionAttribute> qb;
1245     qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id());
1246     qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key);
1247     if (!qb.exec()) {
1248         throw HandlerException("Unable to query for collection attribute");
1249     }
1250 
1251     const QList<CollectionAttribute> result = qb.result();
1252     for (CollectionAttribute attr : result) {
1253         if (!attr.remove()) {
1254             throw HandlerException("Unable to remove collection attribute");
1255         }
1256     }
1257 
1258     if (!result.isEmpty()) {
1259         notificationCollector()->collectionChanged(col, QList<QByteArray>() << key);
1260         return true;
1261     }
1262     return false;
1263 }
1264 
1265 void DataStore::debugLastDbError(QStringView actionDescription) const
1266 {
1267     qCCritical(AKONADISERVER_LOG) << "Database error:" << actionDescription;
1268     qCCritical(AKONADISERVER_LOG) << "  Last driver error:" << m_database.lastError().driverText();
1269     qCCritical(AKONADISERVER_LOG) << "  Last database error:" << m_database.lastError().databaseText();
1270 
1271     if (m_akonadi) {
1272         m_akonadi->tracer().error("DataStore (Database Error)",
1273                                   QStringLiteral("%1\nDriver said: %2\nDatabase said:%3")
1274                                       .arg(actionDescription, m_database.lastError().driverText(), m_database.lastError().databaseText()));
1275     }
1276 }
1277 
1278 void DataStore::debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const
1279 {
1280     qCCritical(AKONADISERVER_LOG) << "Query error:" << actionDescription;
1281     qCCritical(AKONADISERVER_LOG) << "  Last error message:" << query.lastError().text();
1282     qCCritical(AKONADISERVER_LOG) << "  Last driver error:" << m_database.lastError().driverText();
1283     qCCritical(AKONADISERVER_LOG) << "  Last database error:" << m_database.lastError().databaseText();
1284 
1285     if (m_akonadi) {
1286         m_akonadi->tracer().error("DataStore (Database Query Error)",
1287                                   QStringLiteral("%1: %2").arg(QString::fromLatin1(actionDescription), query.lastError().text()));
1288     }
1289 }
1290 
1291 // static
1292 QString DataStore::dateTimeFromQDateTime(const QDateTime &dateTime)
1293 {
1294     QDateTime utcDateTime = dateTime;
1295     if (utcDateTime.timeSpec() != Qt::UTC) {
1296         utcDateTime = utcDateTime.toUTC();
1297     }
1298     return utcDateTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"));
1299 }
1300 
1301 // static
1302 QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime)
1303 {
1304     return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss"));
1305 }
1306 
1307 bool DataStore::doRollback()
1308 {
1309     QSqlDriver *driver = m_database.driver();
1310     QElapsedTimer timer;
1311     timer.start();
1312     driver->rollbackTransaction();
1313     StorageDebugger::instance()->removeTransaction(reinterpret_cast<qint64>(this), false, timer.elapsed(), m_database.lastError().text());
1314     if (m_database.lastError().isValid()) {
1315         debugLastDbError(u"DataStore::rollbackTransaction");
1316         return false;
1317     }
1318     return true;
1319 }
1320 
1321 void DataStore::transactionKilledByDB()
1322 {
1323     m_transactionKilledByDB = true;
1324     cleanupAfterRollback();
1325     Q_EMIT transactionRolledBack();
1326 }
1327 
1328 bool DataStore::beginTransaction(const QString &name)
1329 {
1330     if (!m_dbOpened) {
1331         return false;
1332     }
1333 
1334     if (m_transactionLevel == 0 || m_transactionKilledByDB) {
1335         m_transactionKilledByDB = false;
1336         QElapsedTimer timer;
1337         timer.start();
1338         if (DbType::type(m_database) == DbType::Sqlite) {
1339             m_database.exec(QStringLiteral("BEGIN IMMEDIATE TRANSACTION"));
1340             StorageDebugger::instance()->addTransaction(reinterpret_cast<qint64>(this), name, timer.elapsed(), m_database.lastError().text());
1341             if (m_database.lastError().isValid()) {
1342                 debugLastDbError(QStringLiteral("DataStore::beginTransaction (SQLITE) name: %1").arg(name));
1343                 return false;
1344             }
1345         } else {
1346             m_database.driver()->beginTransaction();
1347             StorageDebugger::instance()->addTransaction(reinterpret_cast<qint64>(this), name, timer.elapsed(), m_database.lastError().text());
1348             if (m_database.lastError().isValid()) {
1349                 debugLastDbError(u"DataStore::beginTransaction");
1350                 return false;
1351             }
1352         }
1353 
1354         if (DbType::type(m_database) == DbType::PostgreSQL) {
1355             // Make constraints check deferred in PostgreSQL. Allows for
1356             // INSERT INTO mimetypetable (name) VALUES ('foo') RETURNING id;
1357             // INSERT INTO collectionmimetyperelation (collection_id, mimetype_id) VALUES (x, y)
1358             // where "y" refers to the newly inserted mimetype
1359             m_database.exec(QStringLiteral("SET CONSTRAINTS ALL DEFERRED"));
1360         }
1361     }
1362 
1363     ++m_transactionLevel;
1364 
1365     return true;
1366 }
1367 
1368 bool DataStore::rollbackTransaction()
1369 {
1370     if (!m_dbOpened) {
1371         return false;
1372     }
1373 
1374     if (m_transactionLevel == 0) {
1375         qCWarning(AKONADISERVER_LOG) << "DataStore::rollbackTransaction(): No transaction in progress!";
1376         return false;
1377     }
1378 
1379     --m_transactionLevel;
1380 
1381     if (m_transactionLevel == 0 && !m_transactionKilledByDB) {
1382         doRollback();
1383         cleanupAfterRollback();
1384         Q_EMIT transactionRolledBack();
1385     }
1386 
1387     return true;
1388 }
1389 
1390 bool DataStore::commitTransaction()
1391 {
1392     if (!m_dbOpened) {
1393         return false;
1394     }
1395 
1396     if (m_transactionLevel == 0) {
1397         qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): No transaction in progress!";
1398         return false;
1399     }
1400 
1401     if (m_transactionLevel == 1) {
1402         if (m_transactionKilledByDB) {
1403             qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): Cannot commit, transaction was killed by mysql deadlock handling!";
1404             return false;
1405         }
1406         QSqlDriver *driver = m_database.driver();
1407         QElapsedTimer timer;
1408         timer.start();
1409         driver->commitTransaction();
1410         StorageDebugger::instance()->removeTransaction(reinterpret_cast<qint64>(this), true, timer.elapsed(), m_database.lastError().text());
1411         if (m_database.lastError().isValid()) {
1412             debugLastDbError(u"DataStore::commitTransaction");
1413             rollbackTransaction();
1414             return false;
1415         } else {
1416             m_transactionLevel--;
1417             Q_EMIT transactionCommitted();
1418         }
1419     } else {
1420         m_transactionLevel--;
1421     }
1422     return true;
1423 }
1424 
1425 bool DataStore::inTransaction() const
1426 {
1427     return m_transactionLevel > 0;
1428 }
1429 
1430 void DataStore::sendKeepAliveQuery()
1431 {
1432     if (m_database.isOpen()) {
1433         QSqlQuery query(m_database);
1434         query.exec(QStringLiteral("SELECT 1"));
1435     }
1436 }
1437 
1438 void DataStore::cleanupAfterRollback()
1439 {
1440     MimeType::invalidateCompleteCache();
1441     Flag::invalidateCompleteCache();
1442     Resource::invalidateCompleteCache();
1443     Collection::invalidateCompleteCache();
1444     PartType::invalidateCompleteCache();
1445     if (m_akonadi) {
1446         m_akonadi->collectionStatistics().expireCache();
1447     }
1448     QueryCache::clear();
1449 }
1450 
1451 #include "moc_datastore.cpp"