Akonadi

datastore.cpp
1/***************************************************************************
2 * SPDX-FileCopyrightText: 2006 Andreas Gungl <a.gungl@gmx.de> *
3 * SPDX-FileCopyrightText: 2007 Robert Zwerus <arzie@dds.nl> *
4 * *
5 * SPDX-License-Identifier: LGPL-2.0-or-later *
6 ***************************************************************************/
7
8#include "datastore.h"
9
10#include "akonadi.h"
11#include "akonadischema.h"
12#include "akonadiserver_debug.h"
13#include "collectionqueryhelper.h"
14#include "collectionstatistics.h"
15#include "dbconfig.h"
16#include "dbinitializer.h"
17#include "dbupdater.h"
18#include "handler.h"
19#include "parthelper.h"
20#include "parttypehelper.h"
21#include "querycache.h"
22#include "selectquerybuilder.h"
23#include "storage/query.h"
24#include "storagedebugger.h"
25#include "tracer.h"
26#include "transaction.h"
27
28#include "private/externalpartstorage_p.h"
29#include <shared/akranges.h>
30
31#include <QCoreApplication>
32#include <QElapsedTimer>
33#include <QFile>
34#include <QSqlDriver>
35#include <QSqlError>
36#include <QSqlQuery>
37#include <QSqlRecord>
38#include <QString>
39#include <QStringList>
40#include <QThread>
41#include <QTimer>
42#include <QUuid>
43#include <QVariant>
44
45#include <functional>
46#include <shared_mutex>
47
48using namespace Akonadi;
49using namespace Akonadi::Server;
50using namespace AkRanges;
51
52static QThreadStorage<DataStore *> sInstances;
53
54class DataStoreDbMap
55{
56public:
57 void registerDataStore(DataStore *store, const QString &connectionName)
58 {
59 std::unique_lock lock{m_mutex};
60 m_table.insert(connectionName, store);
61 }
62
63 void unregisterDataStore(const QString &connectionName)
64 {
65 std::unique_lock lock{m_mutex};
66 m_table.remove(connectionName);
67 }
68
69 DataStore *lookupByConnection(const QSqlDatabase &db)
70 {
71 std::shared_lock lock{m_mutex};
72 auto *store = m_table.value(db.connectionName(), nullptr);
73 Q_ASSERT(store);
74 return store;
75 }
76
77private:
78 std::shared_mutex m_mutex;
80};
81
82static DataStoreDbMap sStoreLookup;
83
84static inline void setBoolPtr(bool *ptr, bool val)
85{
86 if (ptr) {
87 *ptr = val;
88 }
89}
90
91std::unique_ptr<DataStoreFactory> DataStore::sFactory;
92
93void DataStore::setFactory(std::unique_ptr<DataStoreFactory> factory)
94{
95 sFactory = std::move(factory);
96}
97
99{
100 return sStoreLookup.lookupByConnection(db);
101}
102
103/***************************************************************************
104 * DataStore *
105 ***************************************************************************/
106DataStore::DataStore(AkonadiServer *akonadi, DbConfig *dbConfig)
107 : m_akonadi(akonadi)
108 , m_dbConfig(dbConfig)
109 , m_dbOpened(false)
110 , m_transactionLevel(0)
111 , m_keepAliveTimer(nullptr)
112{
113 if (dbConfig->driverName() == QLatin1StringView("QMYSQL")) {
114 // Send a dummy query to MySQL every 1 hour to keep the connection alive,
115 // otherwise MySQL just drops the connection and our subsequent queries fail
116 // without properly reporting the error
117 m_keepAliveTimer = new QTimer(this);
118 m_keepAliveTimer->setInterval(3600 * 1000);
119 QObject::connect(m_keepAliveTimer, &QTimer::timeout, this, &DataStore::sendKeepAliveQuery);
120 }
121}
122
124 : DataStore(nullptr, dbConfig)
125{
126}
127
129{
130 Q_ASSERT_X(!m_dbOpened, "DataStore", "Attempting to destroy DataStore with opened DB connection. Call close() first!");
131}
132
134{
135 m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast<qulonglong>(QThread::currentThread()));
136 Q_ASSERT(!QSqlDatabase::contains(m_connectionName));
137
138 m_database = QSqlDatabase::addDatabase(m_dbConfig->driverName(), m_connectionName);
139 sStoreLookup.registerDataStore(this, m_connectionName);
140 m_dbConfig->apply(m_database);
141
142 if (!m_database.isValid()) {
143 m_dbOpened = false;
144 return;
145 }
146 m_dbOpened = m_database.open();
147
148 if (!m_dbOpened) {
149 qCCritical(AKONADISERVER_LOG) << "Database error: Cannot open database.";
150 qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText();
151 qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText();
152 return;
153 } else {
154 qCDebug(AKONADISERVER_LOG) << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName();
155 }
156
157 StorageDebugger::instance()->addConnection(reinterpret_cast<qint64>(this), QThread::currentThread()->objectName());
159 if (!name.isEmpty()) {
160 StorageDebugger::instance()->changeConnection(reinterpret_cast<qint64>(this), name);
161 }
162 });
163
164 m_dbConfig->initSession(m_database);
165
166 if (m_keepAliveTimer) {
167 m_keepAliveTimer->start();
168 }
169}
170
172{
173 if (!m_dbOpened) {
174 open();
175 }
176 return m_database;
177}
178
180{
181 if (m_keepAliveTimer) {
182 m_keepAliveTimer->stop();
183 }
184
185 if (!m_dbOpened) {
186 return;
187 }
188
189 if (inTransaction()) {
190 // By setting m_transactionLevel to '1' here, we skip all nested transactions
191 // and rollback the outermost transaction.
192 m_transactionLevel = 1;
194 }
195
197 m_database.close();
198 m_database = QSqlDatabase();
199 QSqlDatabase::removeDatabase(m_connectionName);
200 sStoreLookup.unregisterDataStore(m_connectionName);
201
202 StorageDebugger::instance()->removeConnection(reinterpret_cast<qint64>(this));
203
204 m_dbOpened = false;
205}
206
208{
209 // Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread());
210
211 AkonadiSchema schema;
212 DbInitializer::Ptr initializer = DbInitializer::createInstance(m_database, &schema);
213 if (!initializer->run()) {
214 qCCritical(AKONADISERVER_LOG) << initializer->errorMsg();
215 return false;
216 }
217
218 if (QFile::exists(QStringLiteral(":dbupdate.xml"))) {
219 DbUpdater updater(m_database, QStringLiteral(":dbupdate.xml"));
220 if (!updater.run()) {
221 return false;
222 }
223 } else {
224 qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates";
225 }
226
227 if (!initializer->updateIndexesAndConstraints()) {
228 qCCritical(AKONADISERVER_LOG) << initializer->errorMsg();
229 return false;
230 }
231
232 // enable caching for some tables
233 MimeType::enableCache(true);
234 Flag::enableCache(true);
235 Resource::enableCache(true);
236 Collection::enableCache(true);
237 PartType::enableCache(true);
238
239 return true;
240}
241
243{
244 Q_ASSERT(m_akonadi);
245 if (!mNotificationCollector) {
246 mNotificationCollector = std::make_unique<NotificationCollector>(*m_akonadi, this);
247 }
248
249 return mNotificationCollector.get();
250}
251
253{
254 if (!sInstances.hasLocalData()) {
255 sInstances.setLocalData(sFactory->createStore());
256 }
257 return sInstances.localData();
258}
259
261{
262 return sInstances.hasLocalData();
263}
264
265/* --- ItemFlags ----------------------------------------------------- */
266
267bool DataStore::setItemsFlags(const PimItem::List &items,
268 const QList<Flag> *currentFlags,
269 const QList<Flag> &newFlags,
270 bool *flagsChanged,
271 const Collection &col_,
272 bool silent)
273{
274 QSet<QString> removedFlags;
275 QSet<QString> addedFlags;
276 QVariantList insIds;
277 QVariantList insFlags;
278 Query::Condition delConds(Query::Or);
279 Collection col = col_;
280
281 setBoolPtr(flagsChanged, false);
282
283 for (const PimItem &item : items) {
284 const Flag::List itemFlags = currentFlags ? *currentFlags : item.flags(); // optimization
285 for (const Flag &flag : itemFlags) {
286 if (!newFlags.contains(flag)) {
287 removedFlags << flag.name();
288 Query::Condition cond;
289 cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id());
290 cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id());
291 delConds.addCondition(cond);
292 }
293 }
294
295 for (const Flag &flag : newFlags) {
296 if (!itemFlags.contains(flag)) {
297 addedFlags << flag.name();
298 insIds << item.id();
299 insFlags << flag.id();
300 }
301 }
302
303 if (col.id() == -1) {
304 col.setId(item.collectionId());
305 } else if (col.id() != item.collectionId()) {
306 col.setId(-2);
307 }
308 }
309
310 if (!removedFlags.empty()) {
311 QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete);
312 qb.addCondition(delConds);
313 if (!qb.exec()) {
314 return false;
315 }
316 }
317
318 if (!addedFlags.empty()) {
319 QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert);
320 qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds);
321 qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags);
322 qb2.setIdentificationColumn(QString());
323 if (!qb2.exec()) {
324 return false;
325 }
326 }
327
328 if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) {
329 QSet<QByteArray> addedFlagsBa;
330 QSet<QByteArray> removedFlagsBa;
331 for (const auto &addedFlag : std::as_const(addedFlags)) {
332 addedFlagsBa.insert(addedFlag.toLatin1());
333 }
334 for (const auto &removedFlag : std::as_const(removedFlags)) {
335 removedFlagsBa.insert(removedFlag.toLatin1());
336 }
337 notificationCollector()->itemsFlagsChanged(items, addedFlagsBa, removedFlagsBa, col);
338 }
339
340 setBoolPtr(flagsChanged, (addedFlags != removedFlags));
341
342 return true;
343}
344
345bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet<Entity::Id> &existing, const Collection &col_, bool silent)
346{
347 Collection col = col_;
348 QVariantList flagIds;
349 QVariantList appendIds;
350 PimItem::List appendItems;
351 for (const PimItem &item : items) {
352 if (existing.contains(item.id())) {
353 continue;
354 }
355
356 flagIds << flag.id();
357 appendIds << item.id();
358 appendItems << item;
359
360 if (col.id() == -1) {
361 col.setId(item.collectionId());
362 } else if (col.id() != item.collectionId()) {
363 col.setId(-2);
364 }
365 }
366
367 if (appendItems.isEmpty()) {
368 return true; // all items have the desired flags already
369 }
370
371 {
372 QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert);
373 qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds);
374 qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds);
375 qb2.setIdentificationColumn(QString());
376 if (!qb2.exec()) {
377 qCWarning(AKONADISERVER_LOG) << "Failed to append flag" << flag.name() << "to Items" << appendIds;
378 return false;
379 }
380 }
381
382 if (!silent) {
383 notificationCollector()->itemsFlagsChanged(appendItems, {flag.name().toLatin1()}, {}, col);
384 }
385
386 return true;
387}
388
389bool DataStore::appendItemsFlags(const PimItem::List &items,
390 const QList<Flag> &flags,
391 bool *flagsChanged,
392 bool checkIfExists,
393 const Collection &col,
394 bool silent)
395{
396 QVariantList itemsIds;
397 itemsIds.reserve(items.count());
398 for (const PimItem &item : items) {
399 itemsIds.append(item.id());
400 }
401
402 setBoolPtr(flagsChanged, false);
403
404 for (const Flag &flag : flags) {
405 QSet<PimItem::Id> existing;
406 if (checkIfExists) {
407 QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select);
408 Query::Condition cond;
409 cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id());
410 cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds);
411 qb.addColumn(PimItemFlagRelation::leftColumn());
412 qb.addCondition(cond);
413
414 if (!qb.exec()) {
415 qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing flags for Items " << itemsIds;
416 return false;
417 }
418
419 auto &query = qb.query();
420 if (query.driver()->hasFeature(QSqlDriver::QuerySize)) {
421 // The query size feature is not supported by the sqllite driver
422 if (query.size() == items.count()) {
423 continue;
424 }
425 setBoolPtr(flagsChanged, true);
426 }
427
428 while (query.next()) {
429 existing << query.value(0).value<PimItem::Id>();
430 }
431 if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) {
432 if (existing.size() != items.count()) {
433 setBoolPtr(flagsChanged, true);
434 }
435 }
436 }
437
438 if (!doAppendItemsFlag(items, flag, existing, col, silent)) {
439 return false;
440 }
441 }
442
443 return true;
444}
445
446bool DataStore::removeItemsFlags(const PimItem::List &items, const QList<Flag> &flags, bool *flagsChanged, const Collection &col_, bool silent)
447{
448 Collection col = col_;
449 QSet<QString> removedFlags;
450 QVariantList itemsIds;
451 QVariantList flagsIds;
452
453 setBoolPtr(flagsChanged, false);
454 itemsIds.reserve(items.count());
455
456 for (const PimItem &item : items) {
457 itemsIds << item.id();
458 if (col.id() == -1) {
459 col.setId(item.collectionId());
460 } else if (col.id() != item.collectionId()) {
461 col.setId(-2);
462 }
463 for (int i = 0; i < flags.count(); ++i) {
464 const QString flagName = flags[i].name();
465 if (!removedFlags.contains(flagName)) {
466 flagsIds << flags[i].id();
467 removedFlags << flagName;
468 }
469 }
470 }
471
472 // Delete all given flags from all given items in one go
473 QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete);
474 Query::Condition cond(Query::And);
475 cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds);
476 cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds);
477 qb.addCondition(cond);
478 if (!qb.exec()) {
479 qCWarning(AKONADISERVER_LOG) << "Failed to remove flags" << flags << "from Items" << itemsIds;
480 return false;
481 }
482
483 if (qb.query().numRowsAffected() != 0) {
484 qb.query().finish();
485 setBoolPtr(flagsChanged, true);
486 if (!silent) {
487 QSet<QByteArray> removedFlagsBa;
488 for (const auto &remoteFlag : std::as_const(removedFlags)) {
489 removedFlagsBa.insert(remoteFlag.toLatin1());
490 }
491 notificationCollector()->itemsFlagsChanged(items, {}, removedFlagsBa, col);
492 }
493 }
494
495 return true;
496}
497
498/* --- ItemTags ----------------------------------------------------- */
499
500bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent)
501{
502 QList<Tag> removedTags;
503 QList<Tag> addedTags;
504 QVariantList insIds;
505 QVariantList insTags;
506 Query::Condition delConds(Query::Or);
507
508 setBoolPtr(tagsChanged, false);
509
510 for (const PimItem &item : items) {
511 const Tag::List itemTags = item.tags();
512 for (const Tag &tag : itemTags) {
513 if (!tags.contains(tag)) {
514 // Remove tags from items that had it set
515 removedTags.push_back(tag);
516 Query::Condition cond;
517 cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id());
518 cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id());
519 delConds.addCondition(cond);
520 }
521 }
522
523 for (const Tag &tag : tags) {
524 if (!itemTags.contains(tag)) {
525 // Add tags to items that did not have the tag
526 addedTags.push_back(tag);
527 insIds << item.id();
528 insTags << tag.id();
529 }
530 }
531 }
532
533 if (!removedTags.empty()) {
534 QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete);
535 qb.addCondition(delConds);
536 if (!qb.exec()) {
537 qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTags << "from Items";
538 return false;
539 }
540 }
541
542 if (!addedTags.empty()) {
543 QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert);
544 qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds);
545 qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags);
546 qb2.setIdentificationColumn(QString());
547 if (!qb2.exec()) {
548 qCWarning(AKONADISERVER_LOG) << "Failed to add tags" << addedTags << "to Items";
549 return false;
550 }
551 }
552
553 if (!silent && (!addedTags.empty() || !removedTags.empty())) {
554 notificationCollector()->itemsTagsChanged(items, addedTags, removedTags);
555 }
556
557 setBoolPtr(tagsChanged, (addedTags != removedTags));
558
559 return true;
560}
561
562bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet<Entity::Id> &existing, const Collection &col, bool silent)
563{
564 QVariantList tagIds;
565 QVariantList appendIds;
566 PimItem::List appendItems;
567 for (const PimItem &item : items) {
568 if (existing.contains(item.id())) {
569 continue;
570 }
571
572 tagIds << tag.id();
573 appendIds << item.id();
574 appendItems << item;
575 }
576
577 if (appendItems.isEmpty()) {
578 return true; // all items have the desired tags already
579 }
580
581 {
582 QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert);
583 qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds);
584 qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds);
585 qb2.setIdentificationColumn(QString());
586 if (!qb2.exec()) {
587 qCWarning(AKONADISERVER_LOG) << "Failed to append tag" << tag << "to Items" << appendItems;
588 return false;
589 }
590 }
591
592 if (!silent) {
593 notificationCollector()->itemsTagsChanged(appendItems, {tag}, {}, col);
594 }
595
596 return true;
597}
598
599bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool checkIfExists, const Collection &col, bool silent)
600{
601 QVariantList itemsIds;
602 itemsIds.reserve(items.count());
603 for (const PimItem &item : items) {
604 itemsIds.append(item.id());
605 }
606
607 setBoolPtr(tagsChanged, false);
608
609 for (const Tag &tag : tags) {
610 QSet<PimItem::Id> existing;
611 if (checkIfExists) {
612 QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select);
613 Query::Condition cond;
614 cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id());
615 cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds);
616 qb.addColumn(PimItemTagRelation::leftColumn());
617 qb.addCondition(cond);
618
619 if (!qb.exec()) {
620 qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing tag" << tag << "for Items" << itemsIds;
621 return false;
622 }
623
624 auto &query = qb.query();
625 if (query.driver()->hasFeature(QSqlDriver::QuerySize)) {
626 if (query.size() == items.count()) {
627 continue;
628 }
629 setBoolPtr(tagsChanged, true);
630 }
631
632 while (query.next()) {
633 existing << query.value(0).value<PimItem::Id>();
634 }
635 if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) {
636 if (existing.size() != items.count()) {
637 setBoolPtr(tagsChanged, true);
638 }
639 }
640 }
641
642 if (!doAppendItemsTag(items, tag, existing, col, silent)) {
643 return false;
644 }
645 }
646
647 return true;
648}
649
650bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &removedTags, bool *tagsChanged, bool silent)
651{
652 setBoolPtr(tagsChanged, false);
653
654 const auto itemsIds = items | Views::transform([](const auto &item) -> QVariant {
655 return item.id();
656 })
657 | Actions::toQList;
658 const auto tagsIds = removedTags | Views::transform([](const auto &tag) -> QVariant {
659 return tag.id();
660 })
661 | Actions::toQList;
662
663 // Delete all given tags from all given items in one go
664 QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete);
665 Query::Condition cond(Query::And);
666 cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds);
667 cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds);
668 qb.addCondition(cond);
669 if (!qb.exec()) {
670 qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << tagsIds << "from Items" << itemsIds;
671 return false;
672 }
673
674 if (qb.query().numRowsAffected() != 0) {
675 qb.query().finish();
676 setBoolPtr(tagsChanged, true);
677 if (!silent) {
678 notificationCollector()->itemsTagsChanged(items, {}, removedTags);
679 }
680 }
681
682 return true;
683}
684
685bool DataStore::removeTags(const Tag::List &removedTags, bool silent)
686{
687 // Currently the "silent" argument is only for API symmetry
688 Q_UNUSED(silent)
689
690 const auto removedTagsIds = removedTags | Views::transform([](const auto &tag) -> QVariant {
691 return tag.id();
692 })
693 | Actions::toQList;
694
695 // Get all PIM items that we will untag
697 itemsQuery.addColumn(PimItem::collectionIdFullColumnName());
698 itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName());
699 itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds);
700 itemsQuery.addSortColumn(PimItem::collectionIdFullColumnName(), Query::Ascending);
701
702 if (!itemsQuery.exec()) {
703 qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to query Items for given tags" << removedTagsIds;
704 return false;
705 }
706
707 // Emit itemsTagsChanged for all items that have the removed tags.
708 // We group them by collection, since that's what the notification collector as well as
709 // resources expect.
710 PimItem::List items;
711 auto &query = itemsQuery.query();
712 Collection::Id lastCollectionId = -1;
713 const auto collectionIdColumn = query.record().count() - 1;
714 while (query.next()) {
715 const auto collectionId = query.value(collectionIdColumn).value<Collection::Id>();
716 if (!items.empty() && collectionId != lastCollectionId) {
717 notificationCollector()->itemsTagsChanged(items, {}, removedTags, Collection::retrieveById(lastCollectionId));
718 items.clear();
719 }
720
721 items.push_back(PimItem::extractEntity(query));
722 lastCollectionId = collectionId;
723 }
724
725 if (!items.empty()) {
726 notificationCollector()->itemsTagsChanged(items, {}, removedTags, Collection::retrieveById(lastCollectionId));
727 }
728
729 for (const Tag &tag : removedTags) {
730 // Emit special tagRemoved notification for each resource that owns the tag
731 QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select);
732 qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName());
733 qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName());
734 qb.addColumn(Resource::nameFullColumnName());
735 qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id());
736 if (!qb.exec()) {
737 qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to retrieve RIDs for tag" << tag.id();
738 return false;
739 }
740
741 // Emit specialized notifications for each resource
742 auto &query = qb.query();
743 while (query.next()) {
744 const QString rid = query.value(0).toString();
745 const QByteArray resource = query.value(1).toByteArray();
746
747 notificationCollector()->tagRemoved(tag, resource, rid);
748 }
749 query.finish();
750
751 // And one for clients - without RID
753 }
754
755 // Just remove the tags, table constraints will take care of the rest
756 QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete);
757 qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds);
758 if (!qb.exec()) {
759 qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTagsIds;
760 return false;
761 }
762
763 return true;
764}
765
766/* --- ItemParts ----------------------------------------------------- */
767
768bool DataStore::removeItemParts(const PimItem &item, const QSet<QByteArray> &parts)
769{
771 qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName());
772 qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id());
774
775 if (!qb.exec()) {
776 qCWarning(AKONADISERVER_LOG) << "Removing item parts failed: failed to query parts" << parts << "from Item " << item.id();
777 return false;
778 }
779
780 const Part::List existingParts = qb.result();
781 for (Part part : std::as_const(existingParts)) {
782 if (!PartHelper::remove(&part)) {
783 qCWarning(AKONADISERVER_LOG) << "Failed to remove part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name()
784 << ") from Item" << item.id();
785 return false;
786 }
787 }
788 qb.query().finish(); // finish before dispatching notification
789
790 notificationCollector()->itemChanged(item, parts);
791 return true;
792}
793
794bool DataStore::invalidateItemCache(const PimItem &item)
795{
796 // find all payload item parts
798 qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName());
799 qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName());
800 qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id());
801 qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
802 qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1StringView("PLD"));
803 qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false);
804
805 if (!qb.exec()) {
806 qCWarning(AKONADISERVER_LOG) << "Failed to invalidate cache for Item" << item.id();
807 return false;
808 }
809
810 const Part::List parts = qb.result();
811 // clear data field
812 for (Part part : parts) {
813 if (!PartHelper::truncate(part)) {
814 qCWarning(AKONADISERVER_LOG) << "Failed to truncate payload part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name()
815 << ") of Item" << item.id();
816 return false;
817 }
818 }
819
820 return true;
821}
822
823/* --- Collection ------------------------------------------------------ */
824bool DataStore::appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap<QByteArray, QByteArray> &attributes)
825{
826 // no need to check for already existing collection with the same name,
827 // a unique index on parent + name prevents that in the database
828 if (!collection.insert()) {
829 qCWarning(AKONADISERVER_LOG) << "Failed to append Collection" << collection.name() << "in resource" << collection.resource().name();
830 return false;
831 }
832
833 if (!appendMimeTypeForCollection(collection.id(), mimeTypes)) {
834 qCWarning(AKONADISERVER_LOG) << "Failed to append mimetypes" << mimeTypes << "to new collection" << collection.name() << "(ID" << collection.id()
835 << ") in resource" << collection.resource().name();
836 return false;
837 }
838
839 for (auto it = attributes.cbegin(), end = attributes.cend(); it != end; ++it) {
840 if (!addCollectionAttribute(collection, it.key(), it.value(), true)) {
841 qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << it.key() << "to new collection" << collection.name() << "(ID" << collection.id()
842 << ") in resource" << collection.resource().name();
843 return false;
844 }
845 }
846
848 return true;
849}
850
852{
853 // collect item deletion notifications
854 const PimItem::List items = collection.items();
855 const QByteArray resource = collection.resource().name().toLatin1();
856
857 // generate the notification before actually removing the data
858 // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though
859 notificationCollector()->itemsRemoved(items, collection, resource);
860
861 // remove all external payload parts
862 QueryBuilder qb(Part::tableName(), QueryBuilder::Select);
863 qb.addColumn(Part::dataFullColumnName());
864 qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName());
865 qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName());
866 qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id());
867 qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External);
868 qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant());
869 if (!qb.exec()) {
870 qCWarning(AKONADISERVER_LOG) << "Failed to cleanup collection" << collection.name() << "(ID" << collection.id()
871 << "):" << "Failed to query existing payload parts";
872 return false;
873 }
874
875 try {
876 while (qb.query().next()) {
877 ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray()));
878 }
879 } catch (const PartHelperException &e) {
880 qb.query().finish();
881 qCWarning(AKONADISERVER_LOG) << "PartHelperException while cleaning up collection" << collection.name() << "(ID" << collection.id() << "):" << e.what();
882 return false;
883 }
884 qb.query().finish();
885
886 // delete the collection itself, referential actions will do the rest
888 return collection.remove();
889}
890
891static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId)
892{
893 Transaction transaction(DataStore::self(), QStringLiteral("RECURSIVE SET RESOURCEID"));
894
895 QueryBuilder qb(Collection::tableName(), QueryBuilder::Update);
896 qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id());
897 qb.setColumnValue(Collection::resourceIdColumn(), resourceId);
898 qb.setColumnValue(Collection::remoteIdColumn(), QVariant());
899 qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant());
900 if (!qb.exec()) {
901 qCWarning(AKONADISERVER_LOG) << "Failed to set resource ID" << resourceId << "to collection" << collection.name() << "(ID" << collection.id() << ")";
902 return false;
903 }
904
905 // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc)
906 // as well as mark the items dirty to prevent cache purging before they have been written back
907 qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update);
908 qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id());
909 qb.setColumnValue(PimItem::remoteIdColumn(), QVariant());
910 qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant());
912 qb.setColumnValue(PimItem::datetimeColumn(), now);
913 qb.setColumnValue(PimItem::atimeColumn(), now);
914 qb.setColumnValue(PimItem::dirtyColumn(), true);
915 if (!qb.exec()) {
916 qCWarning(AKONADISERVER_LOG) << "Failed reset RID/RREV for PimItems in Collection" << collection.name() << "(ID" << collection.id() << ")";
917 return false;
918 }
919
920 transaction.commit();
921
922 const auto children = collection.children();
923 for (const Collection &col : children) {
924 if (!recursiveSetResourceId(col, resourceId)) {
925 return false;
926 }
927 }
928 return true;
929}
930
931bool DataStore::moveCollection(Collection &collection, const Collection &newParent)
932{
933 if (collection.parentId() == newParent.id()) {
934 return true;
935 }
936
937 if (!m_dbOpened) {
938 return false;
939 }
940
941 if (!newParent.isValid()) {
942 qCWarning(AKONADISERVER_LOG) << "Failed to move collection" << collection.name() << "(ID" << collection.id() << "): invalid destination";
943 return false;
944 }
945
946 const QByteArray oldResource = collection.resource().name().toLatin1();
947
948 int resourceId = collection.resourceId();
949 const Collection source = collection.parent();
950 if (newParent.id() > 0) { // not root
951 resourceId = newParent.resourceId();
952 }
953 if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) {
954 return false;
955 }
956
957 collection.setParentId(newParent.id());
958 if (collection.resourceId() != resourceId) {
959 collection.setResourceId(resourceId);
960 collection.setRemoteId(QString());
961 collection.setRemoteRevision(QString());
962 if (!recursiveSetResourceId(collection, resourceId)) {
963 return false;
964 }
965 }
966
967 if (!collection.update()) {
968 qCWarning(AKONADISERVER_LOG) << "Failed to move Collection" << collection.name() << "(ID" << collection.id() << ")" << "into Collection"
969 << collection.name() << "(ID" << collection.id() << ")";
970 return false;
971 }
972
973 notificationCollector()->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1());
974 return true;
975}
976
977bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes)
978{
979 if (mimeTypes.isEmpty()) {
980 return true;
981 }
982
983 for (const QString &mimeType : mimeTypes) {
984 const auto &mt = MimeType::retrieveByNameOrCreate(mimeType);
985 if (!mt.isValid()) {
986 return false;
987 }
988 if (!Collection::addMimeType(collectionId, mt.id())) {
989 qCWarning(AKONADISERVER_LOG) << "Failed to append mimetype" << mt.name() << "to Collection" << collectionId;
990 return false;
991 }
992 }
993
994 return true;
995}
996
998{
999 if (!col.cachePolicyInherit()) {
1000 return;
1001 }
1002
1003 Collection parent = col;
1004 while (parent.parentId() != 0) {
1005 parent = parent.parent();
1006 if (!parent.cachePolicyInherit()) {
1007 col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval());
1008 col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout());
1009 col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand());
1010 col.setCachePolicyLocalParts(parent.cachePolicyLocalParts());
1011 return;
1012 }
1013 }
1014
1015 // ### system default
1016 col.setCachePolicyCheckInterval(-1);
1017 col.setCachePolicyCacheTimeout(-1);
1018 col.setCachePolicySyncOnDemand(false);
1019 col.setCachePolicyLocalParts(QStringLiteral("ALL"));
1020}
1021
1023{
1025 qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName());
1026 qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id());
1027
1028 if (!qb.exec()) {
1029 qCWarning(AKONADISERVER_LOG) << "Failed to query virtual collections which PimItem" << item.id() << "belongs into";
1030 return QList<Collection>();
1031 }
1032
1033 return qb.result();
1034}
1035
1037{
1038 QueryBuilder qb(CollectionPimItemRelation::tableName(), QueryBuilder::Select);
1039 qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName());
1040 qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), CollectionPimItemRelation::rightFullColumnName());
1041 qb.addColumn(Collection::idFullColumnName());
1042 qb.addColumns(QStringList() << PimItem::idFullColumnName() << PimItem::remoteIdFullColumnName() << PimItem::remoteRevisionFullColumnName()
1043 << PimItem::mimeTypeIdFullColumnName());
1044 qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending);
1045
1046 if (items.count() == 1) {
1047 qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, items.first().id());
1048 } else {
1049 QVariantList ids;
1050 ids.reserve(items.count());
1051 for (const PimItem &item : items) {
1052 ids << item.id();
1053 }
1054 qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::In, ids);
1055 }
1056
1057 if (!qb.exec()) {
1058 qCWarning(AKONADISERVER_LOG) << "Failed to query virtual Collections which PimItems" << items << "belong into";
1060 }
1061
1062 auto &query = qb.query();
1064 query.next();
1065 while (query.isValid()) {
1066 const qlonglong collectionId = query.value(0).toLongLong();
1067 QList<PimItem> &pimItems = map[collectionId];
1068 do {
1069 PimItem item;
1070 item.setId(query.value(1).toLongLong());
1071 item.setRemoteId(query.value(2).toString());
1072 item.setRemoteRevision(query.value(3).toString());
1073 item.setMimeTypeId(query.value(4).toLongLong());
1074 pimItems << item;
1075 } while (query.next() && query.value(0).toLongLong() == collectionId);
1076 }
1077 query.finish();
1078
1079 return map;
1080}
1081
1082/* --- PimItem ------------------------------------------------------- */
1083bool DataStore::appendPimItem(QList<Part> &parts,
1084 const QList<Flag> &flags,
1085 const MimeType &mimetype,
1086 const Collection &collection,
1087 const QDateTime &dateTime,
1088 const QString &remote_id,
1089 const QString &remoteRevision,
1090 const QString &gid,
1091 PimItem &pimItem)
1092{
1093 pimItem.setMimeTypeId(mimetype.id());
1094 pimItem.setCollectionId(collection.id());
1095 if (dateTime.isValid()) {
1096 pimItem.setDatetime(dateTime);
1097 }
1098 if (remote_id.isEmpty()) {
1099 // from application
1100 pimItem.setDirty(true);
1101 } else {
1102 // from resource
1103 pimItem.setRemoteId(remote_id);
1104 pimItem.setDirty(false);
1105 }
1106 pimItem.setRemoteRevision(remoteRevision);
1107 pimItem.setGid(gid);
1108 pimItem.setAtime(QDateTime::currentDateTimeUtc());
1109
1110 if (!pimItem.insert()) {
1111 qCWarning(AKONADISERVER_LOG) << "Failed to append new PimItem into Collection" << collection.name() << "(ID" << collection.id() << ")";
1112 return false;
1113 }
1114
1115 // insert every part
1116 if (!parts.isEmpty()) {
1117 // don't use foreach, the caller depends on knowing the part has changed, see the Append handler
1118 for (QList<Part>::iterator it = parts.begin(); it != parts.end(); ++it) {
1119 (*it).setPimItemId(pimItem.id());
1120 if ((*it).datasize() < (*it).data().size()) {
1121 (*it).setDatasize((*it).data().size());
1122 }
1123
1124 // qCDebug(AKONADISERVER_LOG) << "Insert from DataStore::appendPimItem";
1125 if (!PartHelper::insert(&(*it))) {
1126 qCWarning(AKONADISERVER_LOG) << "Failed to add part" << it->partType().name() << "to new PimItem" << pimItem.id();
1127 return false;
1128 }
1129 }
1130 }
1131
1132 bool seen = false;
1133 for (const Flag &flag : flags) {
1134 seen |= (flag.name() == QLatin1StringView(AKONADI_FLAG_SEEN) || flag.name() == QLatin1StringView(AKONADI_FLAG_IGNORED));
1135 if (!pimItem.addFlag(flag)) {
1136 qCWarning(AKONADISERVER_LOG) << "Failed to add flag" << flag.name() << "to new PimItem" << pimItem.id();
1137 return false;
1138 }
1139 }
1140
1141 // qCDebug(AKONADISERVER_LOG) << "appendPimItem: " << pimItem;
1142
1143 notificationCollector()->itemAdded(pimItem, seen, collection);
1144 return true;
1145}
1146
1147bool DataStore::unhidePimItem(PimItem &pimItem)
1148{
1149 if (!m_dbOpened) {
1150 return false;
1151 }
1152
1153 qCDebug(AKONADISERVER_LOG) << "DataStore::unhidePimItem(" << pimItem << ")";
1154
1155 // FIXME: This is inefficient. Using a bit on the PimItemTable record would probably be some orders of magnitude faster...
1156 return removeItemParts(pimItem, {AKONADI_ATTRIBUTE_HIDDEN});
1157}
1158
1160{
1161 if (!m_dbOpened) {
1162 return false;
1163 }
1164
1165 qCDebug(AKONADISERVER_LOG) << "DataStore::unhideAllPimItems()";
1166
1167 try {
1168 return PartHelper::remove(Part::partTypeIdFullColumnName(), PartTypeHelper::fromFqName(QStringLiteral("ATR"), QStringLiteral("HIDDEN")).id());
1169 } catch (...) {
1170 } // we can live with this failing
1171
1172 return false;
1173}
1174
1175bool DataStore::cleanupPimItems(const PimItem::List &items, bool silent)
1176{
1177 // generate relation removed notifications
1178 if (!silent) {
1179 // generate the notification before actually removing the data
1181 }
1182
1183 // FIXME: Create a single query to do this
1184 for (const auto &item : items) {
1185 if (!item.clearFlags()) {
1186 qCWarning(AKONADISERVER_LOG) << "Failed to clean up flags from PimItem" << item.id();
1187 return false;
1188 }
1189 if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) {
1190 qCWarning(AKONADISERVER_LOG) << "Failed to clean up parts from PimItem" << item.id();
1191 return false;
1192 }
1193 if (!PimItem::remove(PimItem::idColumn(), item.id())) {
1194 qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id();
1195 return false;
1196 }
1197
1198 if (!Entity::clearRelation<CollectionPimItemRelation>(item.id(), Entity::Right)) {
1199 qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id() << "from linked collections";
1200 return false;
1201 }
1202 }
1203
1204 return true;
1205}
1206
1207bool DataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent)
1208{
1210 qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id());
1211 qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key);
1212 if (!qb.exec()) {
1213 qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id()
1214 << "): Failed to query existing attribute";
1215 return false;
1216 }
1217
1218 if (!qb.result().isEmpty()) {
1219 qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id()
1220 << "): Attribute already exists";
1221 return false;
1222 }
1223
1224 CollectionAttribute attr;
1225 attr.setCollectionId(col.id());
1226 attr.setType(key);
1227 attr.setValue(value);
1228
1229 if (!attr.insert()) {
1230 qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() << ")";
1231 return false;
1232 }
1233
1234 if (!silent) {
1236 }
1237 return true;
1238}
1239
1241{
1243 qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id());
1244 qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key);
1245 if (!qb.exec()) {
1246 throw HandlerException("Unable to query for collection attribute");
1247 }
1248
1249 const QList<CollectionAttribute> result = qb.result();
1250 for (CollectionAttribute attr : result) {
1251 if (!attr.remove()) {
1252 throw HandlerException("Unable to remove collection attribute");
1253 }
1254 }
1255
1256 if (!result.isEmpty()) {
1258 return true;
1259 }
1260 return false;
1261}
1262
1263void DataStore::debugLastDbError(QStringView actionDescription) const
1264{
1265 qCCritical(AKONADISERVER_LOG) << "Database error:" << actionDescription;
1266 qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText();
1267 qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText();
1268
1269 if (m_akonadi) {
1270 m_akonadi->tracer().error("DataStore (Database Error)",
1271 QStringLiteral("%1\nDriver said: %2\nDatabase said:%3")
1272 .arg(actionDescription, m_database.lastError().driverText(), m_database.lastError().databaseText()));
1273 }
1274}
1275
1276void DataStore::debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const
1277{
1278 qCCritical(AKONADISERVER_LOG) << "Query error:" << actionDescription;
1279 qCCritical(AKONADISERVER_LOG) << " Last error message:" << query.lastError().text();
1280 qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText();
1281 qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText();
1282
1283 if (m_akonadi) {
1284 m_akonadi->tracer().error("DataStore (Database Query Error)",
1285 QStringLiteral("%1: %2").arg(QString::fromLatin1(actionDescription), query.lastError().text()));
1286 }
1287}
1288
1289// static
1290QString DataStore::dateTimeFromQDateTime(const QDateTime &dateTime)
1291{
1292 QDateTime utcDateTime = dateTime;
1293 if (utcDateTime.timeSpec() != Qt::UTC) {
1294 utcDateTime = utcDateTime.toUTC();
1295 }
1296 return utcDateTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"));
1297}
1298
1299// static
1300QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime)
1301{
1302 return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss"));
1303}
1304
1305bool DataStore::doRollback()
1306{
1307 QSqlDriver *driver = m_database.driver();
1308 QElapsedTimer timer;
1309 timer.start();
1310 driver->rollbackTransaction();
1311 StorageDebugger::instance()->removeTransaction(reinterpret_cast<qint64>(this), false, timer.elapsed(), m_database.lastError().text());
1312 if (m_database.lastError().isValid()) {
1313 debugLastDbError(u"DataStore::rollbackTransaction");
1314 return false;
1315 }
1316 return true;
1317}
1318
1319void DataStore::transactionKilledByDB()
1320{
1321 m_transactionKilledByDB = true;
1322 cleanupAfterRollback();
1324}
1325
1327{
1328 if (!m_dbOpened) {
1329 return false;
1330 }
1331
1332 if (m_transactionLevel == 0 || m_transactionKilledByDB) {
1333 m_transactionKilledByDB = false;
1334 QElapsedTimer timer;
1335 timer.start();
1336 if (DbType::type(m_database) == DbType::Sqlite) {
1337 QSqlQuery query(QStringLiteral("BEGIN IMMEDIATE TRANSACTION"), m_database);
1338 StorageDebugger::instance()->addTransaction(reinterpret_cast<qint64>(this), name, timer.elapsed(), query.lastError().text());
1339 if (query.lastError().isValid()) {
1340 debugLastDbError(QStringLiteral("DataStore::beginTransaction (SQLITE) name: %1").arg(name));
1341 return false;
1342 }
1343 } else {
1344 m_database.driver()->beginTransaction();
1345 StorageDebugger::instance()->addTransaction(reinterpret_cast<qint64>(this), name, timer.elapsed(), m_database.lastError().text());
1346 if (m_database.lastError().isValid()) {
1347 debugLastDbError(u"DataStore::beginTransaction");
1348 return false;
1349 }
1350 }
1351
1352 if (DbType::type(m_database) == DbType::PostgreSQL) {
1353 // Make constraints check deferred in PostgreSQL. Allows for
1354 // INSERT INTO mimetypetable (name) VALUES ('foo') RETURNING id;
1355 // INSERT INTO collectionmimetyperelation (collection_id, mimetype_id) VALUES (x, y)
1356 // where "y" refers to the newly inserted mimetype
1357 QSqlQuery query(QStringLiteral("SET CONSTRAINTS ALL DEFERRED"), m_database);
1358 }
1359 }
1360
1361 ++m_transactionLevel;
1362
1363 return true;
1364}
1365
1367{
1368 if (!m_dbOpened) {
1369 return false;
1370 }
1371
1372 if (m_transactionLevel == 0) {
1373 qCWarning(AKONADISERVER_LOG) << "DataStore::rollbackTransaction(): No transaction in progress!";
1374 return false;
1375 }
1376
1377 --m_transactionLevel;
1378
1379 if (m_transactionLevel == 0 && !m_transactionKilledByDB) {
1380 doRollback();
1381 cleanupAfterRollback();
1383 }
1384
1385 return true;
1386}
1387
1389{
1390 if (!m_dbOpened) {
1391 return false;
1392 }
1393
1394 if (m_transactionLevel == 0) {
1395 qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): No transaction in progress!";
1396 return false;
1397 }
1398
1399 if (m_transactionLevel == 1) {
1400 if (m_transactionKilledByDB) {
1401 qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): Cannot commit, transaction was killed by mysql deadlock handling!";
1402 return false;
1403 }
1404 QSqlDriver *driver = m_database.driver();
1405 QElapsedTimer timer;
1406 timer.start();
1407 driver->commitTransaction();
1408 StorageDebugger::instance()->removeTransaction(reinterpret_cast<qint64>(this), true, timer.elapsed(), m_database.lastError().text());
1409 if (m_database.lastError().isValid()) {
1410 debugLastDbError(u"DataStore::commitTransaction");
1412 return false;
1413 } else {
1414 m_transactionLevel--;
1416 }
1417 } else {
1418 m_transactionLevel--;
1419 }
1420 return true;
1421}
1422
1424{
1425 return m_transactionLevel > 0;
1426}
1427
1428void DataStore::sendKeepAliveQuery()
1429{
1430 if (m_database.isOpen()) {
1431 QSqlQuery query(m_database);
1432 query.exec(QStringLiteral("SELECT 1"));
1433 }
1434}
1435
1436void DataStore::cleanupAfterRollback()
1437{
1438 MimeType::invalidateCompleteCache();
1439 Flag::invalidateCompleteCache();
1440 Resource::invalidateCompleteCache();
1441 Collection::invalidateCompleteCache();
1442 PartType::invalidateCompleteCache();
1443 if (m_akonadi) {
1444 m_akonadi->collectionStatistics().expireCache();
1445 }
1447}
1448
1449#include "moc_datastore.cpp"
Represents a collection of PIM items.
Definition collection.h:62
qint64 Id
Describes the unique id type.
Definition collection.h:79
void setRemoteId(const QString &id)
Sets the remote id of the collection.
void setRemoteRevision(const QString &revision)
Sets the remote revision of the collection.
void setId(Id identifier)
Sets the unique identifier of the collection.
This class handles all the database access.
Definition datastore.h:95
virtual bool beginTransaction(const QString &name)
Begins a transaction.
QList< Collection > virtualCollections(const PimItem &item)
Returns all virtual collections the item is linked to.
virtual bool moveCollection(Collection &collection, const Collection &newParent)
moves the collection collection to newParent.
virtual bool cleanupPimItems(const PimItem::List &items, bool silent=false)
Removes the pim item and all referenced data ( e.g.
void close()
Closes the database connection.
virtual bool cleanupCollection(Collection &collection)
removes the given collection and all its content
virtual bool rollbackTransaction()
Reverts all changes within the current transaction.
bool inTransaction() const
Returns true if there is a transaction in progress.
static DataStore * self()
Per thread singleton.
~DataStore() override
Closes the database connection and destroys the DataStore object.
void transactionRolledBack()
Emitted if a transaction has been aborted.
virtual bool unhideAllPimItems()
Unhides all the items which have the "hidden" flag set.
void transactionCommitted()
Emitted if a transaction has been successfully committed.
virtual bool unhidePimItem(PimItem &pimItem)
Unhides the specified PimItem.
NotificationCollector * notificationCollector()
Returns the notification collector of this DataStore object.
virtual void open()
Opens the database connection.
DataStore(AkonadiServer *akonadi, DbConfig *dbConfig)
Creates a new DataStore object and opens it.
virtual bool init()
Initializes the database.
static bool hasDataStore()
Returns whether per thread DataStore has been created.
static DataStore * dataStoreForDatabase(const QSqlDatabase &db)
Returns DataStore associated with the given database connection.
Definition datastore.cpp:98
QSqlDatabase database()
Returns the QSqlDatabase object.
virtual bool removeCollectionAttribute(const Collection &col, const QByteArray &key)
Removes the given collection attribute for col.
virtual bool commitTransaction()
Commits all changes within the current transaction and emits all collected notification signals.
virtual void activeCachePolicy(Collection &col)
Determines the active cache policy for this Collection.
A base class that provides an unique access layer to configuration and initialization of different da...
Definition dbconfig.h:21
virtual void apply(QSqlDatabase &database)=0
This method applies the configured settings to the QtSql database instance.
virtual void initSession(const QSqlDatabase &database)
Do session setup/initialization work on database.
Definition dbconfig.cpp:152
virtual QString driverName() const =0
Returns the name of the used driver.
static DbInitializer::Ptr createInstance(const QSqlDatabase &database, Schema *schema=nullptr)
Returns an initializer instance for a given backend.
Updates the database schema.
Definition dbupdater.h:46
bool run()
Starts the update process.
Definition dbupdater.cpp:43
static bool clearRelation(qint64 id, RelationSide side=Left)
Clears all entries from a n:m relation table (specified by the given template parameter).
Definition entity.h:140
Part of the DataStore, collects change notifications and emits them after the current transaction has...
void collectionRemoved(const Collection &collection, const QByteArray &resource=QByteArray())
Notify about a removed collection.
void tagRemoved(const Tag &tag, const QByteArray &resource, const QString &remoteId)
Notify about a removed tag.
void itemsTagsChanged(const PimItem::List &items, const QList< Tag > &addedTags, const QList< Tag > &removedTags, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about changed items tags.
void collectionMoved(const Collection &collection, const Collection &source, const QByteArray &resource=QByteArray(), const QByteArray &destResource=QByteArray())
Notify about a moved collection.
void collectionChanged(const Collection &collection, const QList< QByteArray > &changes, const QByteArray &resource=QByteArray())
Notify about a changed collection.
void itemsRemoved(const PimItem::List &items, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about removed items.
void collectionAdded(const Collection &collection, const QByteArray &resource=QByteArray())
Notify about a added collection.
void itemsFlagsChanged(const PimItem::List &items, const QSet< QByteArray > &addedFlags, const QSet< QByteArray > &removedFlags, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about changed items flags Provide as many parameters as you have at hand currently,...
void itemChanged(const PimItem &item, const QSet< QByteArray > &changedParts, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about a changed item.
void itemAdded(const PimItem &item, bool seen, const Collection &collection=Collection(), const QByteArray &resource=QByteArray())
Notify about an added item.
Helper class to construct arbitrary SQL queries.
void addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type=WhereCondition)
Add a WHERE or HAVING condition which compares a column with a given value.
void addSortColumn(const QString &column, Query::SortOrder order=Query::Ascending)
Add sort column.
void addJoin(JoinType joinType, const QString &table, const Query::Condition &condition)
Join a table to the query.
bool exec()
Executes the query, returns true on success.
void addColumns(const QStringList &cols)
Adds the given columns to a select query.
void addCondition(const Query::Condition &condition, ConditionType type=WhereCondition)
Add a WHERE condition.
void setColumnValue(const QString &column, const QVariant &value)
Sets a column to the given value (only valid for INSERT and UPDATE queries).
QSqlQuery & query()
Returns the query, only valid after exec().
void addColumn(const QString &col)
Adds the given column to a select query.
@ InnerJoin
NOTE: only supported for UPDATE and SELECT queries.
@ LeftJoin
NOTE: only supported for SELECT queries.
Represents a WHERE condition tree.
Definition query.h:62
void addValueCondition(const QString &column, CompareOperator op, const QVariant &value)
Add a WHERE condition which compares a column with a given value.
Definition query.cpp:12
Helper class for creating and executing database SELECT queries.
QList< T > result()
Returns the result of this SELECT query.
void error(const QString &componentName, const QString &msg) override
This method is called whenever a component wants to output an error.
Definition tracer.cpp:125
Helper class for DataStore transaction handling.
Definition transaction.h:23
An Akonadi Tag.
Definition tag.h:26
Id id() const
Returns the unique identifier of the tag.
Definition tag.cpp:139
bool canBeMovedTo(const Collection &collection, const Collection &parent)
Checks if a collection could be moved from its current parent into the given one.
Type type(const QSqlDatabase &db)
Returns the type of the given database object.
Definition dbtype.cpp:11
bool insert(Part *part, qint64 *insertId=nullptr)
Adds a new part to the database and if necessary to the filesystem.
bool truncate(Part &part)
Truncate the payload of part and update filesystem/database accordingly.
bool remove(Part *part)
Deletes part from the database and also removes existing filesystem data if needed.
Query::Condition conditionFromFqNames(const QStringList &fqNames)
Returns a query condition that matches the given part type list.
PartType fromFqName(const QString &fqName)
Retrieve (or create) PartType for the given fully qualified name.
void clear()
Clears all queries from current thread.
Helper integration between Akonadi and Qt.
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
QDateTime currentDateTimeUtc()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
Qt::TimeSpec timeSpec() const const
QString toString(QStringView format, QCalendar cal) const const
QDateTime toUTC() const const
qint64 elapsed() const const
bool exists() const const
iterator begin()
bool contains(const AT &value) const const
qsizetype count() const const
bool empty() const const
iterator end()
bool isEmpty() const const
void push_back(parameter_type value)
qsizetype size() const const
T value(qsizetype i) const const
const_iterator cbegin() const const
const_iterator cend() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void objectNameChanged(const QString &objectName)
QObject * parent() const const
bool contains(const QSet< T > &other) const const
bool empty() const const
iterator insert(const T &value)
bool isEmpty() const const
qsizetype size() const const
QSqlDatabase addDatabase(QSqlDriver *driver, const QString &connectionName)
QString connectionName() const const
bool contains(const QString &connectionName)
QString databaseName() const const
QSqlDriver * driver() const const
QString driverName() const const
bool isOpen() const const
bool isValid() const const
QSqlError lastError() const const
void removeDatabase(const QString &connectionName)
virtual bool beginTransaction()
virtual bool commitTransaction()
virtual bool rollbackTransaction()
QString databaseText() const const
QString driverText() const const
bool isValid() const const
QString text() const const
void finish()
bool next()
QVariant value(const QString &name) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QThread * currentThread()
void setInterval(int msec)
void start()
void stop()
void timeout()
QUuid createUuid()
QString toString(StringFormat mode) const const
QByteArray toByteArray() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:01:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.