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"