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;
79 QHash<QString, DataStore *> m_table;
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
696 SelectQueryBuilder<PimItem> itemsQuery;
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
752 notificationCollector()->tagRemoved(tag, QByteArray(), QString());
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{
770 SelectQueryBuilder<Part> qb;
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
797 SelectQueryBuilder<Part> qb;
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";
1059 return QMap<Entity::Id, QList<PimItem>>();
1060 }
1061
1062 auto &query = qb.query();
1063 QMap<Entity::Id, QList<PimItem>> map;
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) {
1235 notificationCollector()->collectionChanged(col, QList<QByteArray>() << key);
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 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
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 QString &fileName)
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)
bool isOpen() const const
QSqlError lastError() const const
void removeDatabase(const QString &connectionName)
virtual bool commitTransaction()
virtual bool rollbackTransaction()
QString databaseText() const const
QString driverText() 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 timeout()
QUuid createUuid()
QString toString(StringFormat mode) const const
QByteArray toByteArray() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:49:57 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.