diff --git a/src/server/cachecleaner.cpp b/src/server/cachecleaner.cpp index cee0665bb..ca6f5e943 100644 --- a/src/server/cachecleaner.cpp +++ b/src/server/cachecleaner.cpp @@ -1,153 +1,153 @@ /* Copyright (c) 2007 Volker Krause Copyright (C) 2014 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cachecleaner.h" #include "storage/parthelper.h" #include "storage/datastore.h" #include "storage/selectquerybuilder.h" #include "storage/entity.h" #include "akonadi.h" #include "akonadiserver_debug.h" #include #include using namespace Akonadi::Server; QMutex CacheCleanerInhibitor::sLock; int CacheCleanerInhibitor::sInhibitCount = 0; CacheCleanerInhibitor::CacheCleanerInhibitor(bool doInhibit) : mInhibited(false) { if (doInhibit) { inhibit(); } } CacheCleanerInhibitor::~CacheCleanerInhibitor() { if (mInhibited) { uninhibit(); } } void CacheCleanerInhibitor::inhibit() { if (mInhibited) { qCCritical(AKONADISERVER_LOG) << "Cannot recursively inhibit an inhibitor"; return; } sLock.lock(); if (++sInhibitCount == 1) { if (AkonadiServer::instance()->cacheCleaner()) { AkonadiServer::instance()->cacheCleaner()->inhibit(true); } } sLock.unlock(); mInhibited = true; } void CacheCleanerInhibitor::uninhibit() { if (!mInhibited) { qCCritical(AKONADISERVER_LOG) << "Cannot uninhibit an uninhibited inhibitor"; // aaaw yeah return; } mInhibited = false; sLock.lock(); Q_ASSERT(sInhibitCount > 0); if (--sInhibitCount == 0) { if (AkonadiServer::instance()->cacheCleaner()) { AkonadiServer::instance()->cacheCleaner()->inhibit(false); } } sLock.unlock(); } CacheCleaner::CacheCleaner(QObject *parent) : CollectionScheduler(QStringLiteral("CacheCleaner"), QThread::IdlePriority, parent) { setMinimumInterval(5); } CacheCleaner::~CacheCleaner() { quitThread(); } int CacheCleaner::collectionScheduleInterval(const Collection &collection) { return collection.cachePolicyCacheTimeout(); } bool CacheCleaner::hasChanged(const Collection &collection, const Collection &changed) { return collection.cachePolicyLocalParts() != changed.cachePolicyLocalParts() || collection.cachePolicyCacheTimeout() != changed.cachePolicyCacheTimeout() || collection.cachePolicyInherit() != changed.cachePolicyInherit(); } bool CacheCleaner::shouldScheduleCollection(const Collection &collection) { return collection.cachePolicyLocalParts() != QLatin1String("ALL") && collection.cachePolicyCacheTimeout() >= 0 && (collection.enabled() || (collection.displayPref() == Collection::True) || (collection.syncPref() == Collection::True) || (collection.indexPref() == Collection::True)) && collection.resourceId() > 0; } void CacheCleaner::collectionExpired(const Collection &collection) { SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdColumn(), PimItem::idFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, collection.id()); - qb.addValueCondition(PimItem::atimeFullColumnName(), Query::Less, QDateTime::currentDateTime().addSecs(-60 * collection.cachePolicyCacheTimeout())); + qb.addValueCondition(PimItem::atimeFullColumnName(), Query::Less, QDateTime::currentDateTimeUtc().addSecs(-60 * collection.cachePolicyCacheTimeout())); qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); const QStringList partNames = collection.cachePolicyLocalParts().split(QLatin1Char(' ')); for (QString partName : partNames) { if (partName.startsWith(QLatin1String(AKONADI_PARAM_PLD))) { partName = partName.mid(4); } qb.addValueCondition(PartType::nameFullColumnName(), Query::NotEquals, partName); } if (qb.exec()) { const Part::List parts = qb.result(); if (!parts.isEmpty()) { qCDebug(AKONADISERVER_LOG) << "found" << parts.count() << "item parts to expire in collection" << collection.name(); // clear data field for (Part part : parts) { try { if (!PartHelper::truncate(part)) { qCDebug(AKONADISERVER_LOG) << "failed to update item part" << part.id(); } } catch (const PartHelperException &e) { qCCritical(AKONADISERVER_LOG) << e.type() << e.what(); } } } } } diff --git a/src/server/handler/akappend.cpp b/src/server/handler/akappend.cpp index 821030008..e5887447c 100644 --- a/src/server/handler/akappend.cpp +++ b/src/server/handler/akappend.cpp @@ -1,441 +1,441 @@ /*************************************************************************** * Copyright (C) 2007 by Robert Zwerus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "akappend.h" #include "fetchhelper.h" #include "connection.h" #include "preprocessormanager.h" #include "handlerhelper.h" #include "storage/datastore.h" #include "storage/transaction.h" #include "storage/parttypehelper.h" #include "storage/dbconfig.h" #include "storage/partstreamer.h" #include "storage/parthelper.h" #include "storage/selectquerybuilder.h" #include #include //std::accumulate using namespace Akonadi; using namespace Akonadi::Server; static QVector localFlagsToPreserve = QVector() << "$ATTACHMENT" << "$INVITATION" << "$ENCRYPTED" << "$SIGNED" << "$WATCHED"; bool AkAppend::buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &item, Collection &parentCol) { parentCol = HandlerHelper::collectionFromScope(cmd.collection(), connection()); if (!parentCol.isValid()) { return failureResponse("Invalid parent collection"); } if (parentCol.isVirtual()) { return failureResponse("Cannot append item into virtual collection"); } MimeType mimeType = MimeType::retrieveByNameOrCreate(cmd.mimeType()); if (!mimeType.isValid()) { return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'.")); } item.setRev(0); item.setSize(cmd.itemSize()); item.setMimeTypeId(mimeType.id()); item.setCollectionId(parentCol.id()); item.setDatetime(cmd.dateTime()); if (cmd.remoteId().isEmpty()) { // from application item.setDirty(true); } else { // from resource item.setRemoteId(cmd.remoteId()); item.setDirty(false); } item.setRemoteRevision(cmd.remoteRevision()); item.setGid(cmd.gid()); - item.setAtime(QDateTime::currentDateTime()); + item.setAtime(QDateTime::currentDateTimeUtc()); return true; } bool AkAppend::insertItem(const Protocol::CreateItemCommand &cmd, PimItem &item, const Collection &parentCol) { if (!item.datetime().isValid()) { item.setDatetime(QDateTime::currentDateTimeUtc()); } if (!item.insert()) { return failureResponse("Failed to append item"); } // set message flags const QSet flags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.flags() : cmd.addedFlags(); if (!flags.isEmpty()) { // This will hit an entry in cache inserted there in buildPimItem() const Flag::List flagList = HandlerHelper::resolveFlags(flags); bool flagsChanged = false; if (!DataStore::self()->appendItemsFlags(PimItem::List() << item, flagList, &flagsChanged, false, parentCol, true)) { return failureResponse("Unable to append item flags."); } } const Scope tags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.tags() : cmd.addedTags(); if (!tags.isEmpty()) { const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); bool tagsChanged = false; if (!DataStore::self()->appendItemsTags(PimItem::List() << item, tagList, &tagsChanged, false, parentCol, true)) { return failureResponse("Unable to append item tags."); } } // Handle individual parts qint64 partSizes = 0; PartStreamer streamer(connection(), item, this); connect(&streamer, &PartStreamer::responseAvailable, this, static_cast(&Handler::sendResponse)); Q_FOREACH (const QByteArray &partName, cmd.parts()) { qint64 partSize = 0; if (!streamer.stream(true, partName, partSize)) { return failureResponse(streamer.error()); } partSizes += partSize; } const Protocol::Attributes attrs = cmd.attributes(); for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { if (!streamer.streamAttribute(true, iter.key(), iter.value())) { return failureResponse(streamer.error()); } } // TODO: Try to avoid this addition query if (partSizes > item.size()) { item.setSize(partSizes); item.update(); } // Preprocessing if (PreprocessorManager::instance()->isActive()) { Part hiddenAttribute; hiddenAttribute.setPimItemId(item.id()); hiddenAttribute.setPartType(PartTypeHelper::fromFqName(QStringLiteral(AKONADI_ATTRIBUTE_HIDDEN))); hiddenAttribute.setData(QByteArray()); hiddenAttribute.setDatasize(0); // TODO: Handle errors? Technically, this is not a critical issue as no data are lost PartHelper::insert(&hiddenAttribute); } const bool seen = flags.contains(AKONADI_FLAG_SEEN) || flags.contains(AKONADI_FLAG_IGNORED); notify(item, seen, item.collection()); sendResponse(item, Protocol::CreateItemCommand::None); return true; } bool AkAppend::mergeItem(const Protocol::CreateItemCommand &cmd, PimItem &newItem, PimItem ¤tItem, const Collection &parentCol) { bool needsUpdate = false; QSet changedParts; if (!newItem.remoteId().isEmpty() && currentItem.remoteId() != newItem.remoteId()) { currentItem.setRemoteId(newItem.remoteId()); changedParts.insert(AKONADI_PARAM_REMOTEID); needsUpdate = true; } if (!newItem.remoteRevision().isEmpty() && currentItem.remoteRevision() != newItem.remoteRevision()) { currentItem.setRemoteRevision(newItem.remoteRevision()); changedParts.insert(AKONADI_PARAM_REMOTEREVISION); needsUpdate = true; } if (!newItem.gid().isEmpty() && currentItem.gid() != newItem.gid()) { currentItem.setGid(newItem.gid()); changedParts.insert(AKONADI_PARAM_GID); needsUpdate = true; } if (newItem.datetime().isValid() && newItem.datetime() != currentItem.datetime()) { currentItem.setDatetime(newItem.datetime()); needsUpdate = true; } if (newItem.size() > 0 && newItem.size() != currentItem.size()) { currentItem.setSize(newItem.size()); needsUpdate = true; } const Collection col = Collection::retrieveById(parentCol.id()); if (cmd.flags().isEmpty() && !cmd.flagsOverwritten()) { bool flagsAdded = false, flagsRemoved = false; if (!cmd.addedFlags().isEmpty()) { const Flag::List addedFlags = HandlerHelper::resolveFlags(cmd.addedFlags()); DataStore::self()->appendItemsFlags(PimItem::List() << currentItem, addedFlags, &flagsAdded, true, col, true); } if (!cmd.removedFlags().isEmpty()) { const Flag::List removedFlags = HandlerHelper::resolveFlags(cmd.removedFlags()); DataStore::self()->removeItemsFlags(PimItem::List() << currentItem, removedFlags, &flagsRemoved, col, true); } if (flagsAdded || flagsRemoved) { changedParts.insert(AKONADI_PARAM_FLAGS); needsUpdate = true; } } else { bool flagsChanged = false; QSet flagNames = cmd.flags(); // Make sure we don't overwrite some local-only flags that can't come // through from Resource during ItemSync, like $ATTACHMENT, because the // resource is not aware of them (they are usually assigned by client // upon inspecting the payload) Q_FOREACH (const Flag ¤tFlag, currentItem.flags()) { const QByteArray currentFlagName = currentFlag.name().toLatin1(); if (localFlagsToPreserve.contains(currentFlagName)) { flagNames.insert(currentFlagName); } } const Flag::List flags = HandlerHelper::resolveFlags(flagNames); DataStore::self()->setItemsFlags(PimItem::List() << currentItem, flags, &flagsChanged, col, true); if (flagsChanged) { changedParts.insert(AKONADI_PARAM_FLAGS); needsUpdate = true; } } if (cmd.tags().isEmpty()) { bool tagsAdded = false, tagsRemoved = false; if (!cmd.addedTags().isEmpty()) { const Tag::List addedTags = HandlerHelper::tagsFromScope(cmd.addedTags(), connection()); DataStore::self()->appendItemsTags(PimItem::List() << currentItem, addedTags, &tagsAdded, true, col, true); } if (!cmd.removedTags().isEmpty()) { const Tag::List removedTags = HandlerHelper::tagsFromScope(cmd.removedTags(), connection()); DataStore::self()->removeItemsTags(PimItem::List() << currentItem, removedTags, &tagsRemoved, true); } if (tagsAdded || tagsRemoved) { changedParts.insert(AKONADI_PARAM_TAGS); needsUpdate = true; } } else { bool tagsChanged = false; const Tag::List tags = HandlerHelper::tagsFromScope(cmd.tags(), connection()); DataStore::self()->setItemsTags(PimItem::List() << currentItem, tags, &tagsChanged, true); if (tagsChanged) { changedParts.insert(AKONADI_PARAM_TAGS); needsUpdate = true; } } const Part::List existingParts = Part::retrieveFiltered(Part::pimItemIdColumn(), currentItem.id()); QMap partsSizes; for (const Part &part : existingParts) { partsSizes.insert(PartTypeHelper::fullName(part.partType()).toLatin1(), part.datasize()); } PartStreamer streamer(connection(), currentItem); connect(&streamer, &PartStreamer::responseAvailable, this, static_cast(&Handler::sendResponse)); Q_FOREACH (const QByteArray &partName, cmd.parts()) { bool changed = false; qint64 partSize = 0; if (!streamer.stream(true, partName, partSize, &changed)) { return failureResponse(streamer.error()); } if (changed) { changedParts.insert(partName); partsSizes.insert(partName, partSize); needsUpdate = true; } } const qint64 size = std::accumulate(partsSizes.begin(), partsSizes.end(), 0); if (size > currentItem.size()) { currentItem.setSize(size); needsUpdate = true; } if (needsUpdate) { currentItem.setRev(qMax(newItem.rev(), currentItem.rev()) + 1); - currentItem.setAtime(QDateTime::currentDateTime()); + currentItem.setAtime(QDateTime::currentDateTimeUtc()); // Only mark dirty when merged from application currentItem.setDirty(!connection()->context()->resource().isValid()); // Store all changes if (!currentItem.update()) { return failureResponse("Failed to store merged item"); } notify(currentItem, currentItem.collection(), changedParts); } sendResponse(currentItem, cmd.mergeModes()); return true; } bool AkAppend::sendResponse(const PimItem &item, Protocol::CreateItemCommand::MergeModes mergeModes) { if (mergeModes & Protocol::CreateItemCommand::Silent || mergeModes & Protocol::CreateItemCommand::None) { auto resp = Protocol::FetchItemsResponsePtr::create(); resp->setId(item.id()); resp->setMTime(item.datetime()); Handler::sendResponse(resp); return true; } Protocol::FetchScope fetchScope; fetchScope.setAncestorDepth(Protocol::FetchScope::ParentAncestor); fetchScope.setFetch(Protocol::FetchScope::AllAttributes | Protocol::FetchScope::FullPayload | Protocol::FetchScope::CacheOnly | Protocol::FetchScope::Flags | Protocol::FetchScope::GID | Protocol::FetchScope::MTime | Protocol::FetchScope::RemoteID | Protocol::FetchScope::RemoteRevision | Protocol::FetchScope::Size | Protocol::FetchScope::Tags); fetchScope.setTagFetchScope({ "GID" }); ImapSet set; set.add(QVector() << item.id()); Scope scope; scope.setUidSet(set); FetchHelper fetchHelper(connection(), scope, fetchScope); if (!fetchHelper.fetchItems()) { return failureResponse("Failed to retrieve item"); } return true; } bool AkAppend::notify(const PimItem &item, bool seen, const Collection &collection) { DataStore::self()->notificationCollector()->itemAdded(item, seen, collection); if (PreprocessorManager::instance()->isActive()) { // enqueue the item for preprocessing PreprocessorManager::instance()->beginHandleItem(item, DataStore::self()); } return true; } bool AkAppend::notify(const PimItem &item, const Collection &collection, const QSet &changedParts) { if (!changedParts.isEmpty()) { DataStore::self()->notificationCollector()->itemChanged(item, changedParts, collection); } return true; } bool AkAppend::parseStream() { const auto &cmd = Protocol::cmdCast(m_command); // FIXME: The streaming/reading of all item parts can hold the transaction for // unnecessary long time -> should we wrap the PimItem into one transaction // and try to insert Parts independently? In case we fail to insert a part, // it's not a problem as it can be re-fetched at any time, except for attributes. DataStore *db = DataStore::self(); Transaction transaction(db, QStringLiteral("AKAPPEND")); ExternalPartStorageTransaction storageTrx; PimItem item; Collection parentCol; if (!buildPimItem(cmd, item, parentCol)) { return false; } if (cmd.mergeModes() == Protocol::CreateItemCommand::None) { if (!insertItem(cmd, item, parentCol)) { return false; } if (!transaction.commit()) { return failureResponse("Failed to commit transaction"); } storageTrx.commit(); } else { // Merging is always restricted to the same collection SelectQueryBuilder qb; qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, parentCol.id()); if (cmd.mergeModes() & Protocol::CreateItemCommand::GID) { qb.addValueCondition(PimItem::gidColumn(), Query::Equals, item.gid()); } if (cmd.mergeModes() & Protocol::CreateItemCommand::RemoteID) { qb.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); } if (!qb.exec()) { return failureResponse("Failed to query database for item"); } const QVector result = qb.result(); if (result.isEmpty()) { // No item with such GID/RID exists, so call AkAppend::insert() and behave // like if this was a new item if (!insertItem(cmd, item, parentCol)) { return false; } if (!transaction.commit()) { return failureResponse("Failed to commit transaction"); } storageTrx.commit(); } else if (result.count() == 1) { // Item with matching GID/RID combination exists, so merge this item into it // and send itemChanged() PimItem existingItem = result.at(0); if (!mergeItem(cmd, item, existingItem, parentCol)) { return false; } if (!transaction.commit()) { return failureResponse("Failed to commit transaction"); } storageTrx.commit(); } else { qCDebug(AKONADISERVER_LOG) << "Multiple merge candidates:"; Q_FOREACH (const PimItem &item, result) { qCDebug(AKONADISERVER_LOG) << "\tID:" << item.id() << ", RID:" << item.remoteId() << ", GID:" << item.gid() << ", Collection:" << item.collection().name() << "(" << item.collectionId() << ")" << ", Resource:" << item.collection().resource().name() << "(" << item.collection().resourceId() << ")"; } // Nor GID or RID are guaranteed to be unique, so make sure we don't merge // something we don't want return failureResponse("Multiple merge candidates, aborting"); } } return successResponse(); } diff --git a/src/server/handler/copy.cpp b/src/server/handler/copy.cpp index 4e0691aba..3cb032bfd 100644 --- a/src/server/handler/copy.cpp +++ b/src/server/handler/copy.cpp @@ -1,124 +1,124 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "copy.h" #include "connection.h" #include "handlerhelper.h" #include "cachecleaner.h" #include "storage/datastore.h" #include "storage/itemqueryhelper.h" #include "storage/itemretriever.h" #include "storage/selectquerybuilder.h" #include "storage/transaction.h" #include "storage/parthelper.h" #include using namespace Akonadi; using namespace Akonadi::Server; bool Copy::copyItem(const PimItem &item, const Collection &target) { PimItem newItem = item; newItem.setId(-1); newItem.setRev(0); - newItem.setDatetime(QDateTime::currentDateTime()); - newItem.setAtime(QDateTime::currentDateTime()); + newItem.setDatetime(QDateTime::currentDateTimeUtc()); + newItem.setAtime(QDateTime::currentDateTimeUtc()); newItem.setRemoteId(QString()); newItem.setRemoteRevision(QString()); newItem.setCollectionId(target.id()); Part::List parts; parts.reserve(item.parts().count()); Q_FOREACH (const Part &part, item.parts()) { Part newPart(part); newPart.setData(PartHelper::translateData(newPart.data(), part.storage())); newPart.setPimItemId(-1); newPart.setStorage(Part::Internal); parts << newPart; } DataStore *store = connection()->storageBackend(); - if (!store->appendPimItem(parts, item.flags(), item.mimeType(), target, QDateTime::currentDateTime(), QString(), QString(), item.gid(), newItem)) { + if (!store->appendPimItem(parts, item.flags(), item.mimeType(), target, QDateTime::currentDateTimeUtc(), QString(), QString(), item.gid(), newItem)) { return false; } return true; } void Copy::itemsRetrieved(const QList &ids) { SelectQueryBuilder qb; ItemQueryHelper::itemSetToQuery(ImapSet(ids), qb); if (!qb.exec()) { failureResponse("Unable to retrieve items"); return; } const PimItem::List items = qb.result(); qb.query().finish(); DataStore *store = connection()->storageBackend(); Transaction transaction(store, QStringLiteral("COPY")); for (const PimItem &item : items) { if (!copyItem(item, mTargetCollection)) { failureResponse("Unable to copy item"); return; } } if (!transaction.commit()) { failureResponse("Cannot commit transaction."); return; } } bool Copy::parseStream() { const auto &cmd = Protocol::cmdCast(m_command); if (!checkScopeConstraints(cmd.items(), Scope::Uid)) { return failureResponse("Only UID copy is allowed"); } if (cmd.items().isEmpty()) { return failureResponse("No items specified"); } mTargetCollection = HandlerHelper::collectionFromScope(cmd.destination(), connection()); if (!mTargetCollection.isValid()) { return failureResponse("No valid target specified"); } if (mTargetCollection.isVirtual()) { return failureResponse("Copying items into virtual collections is not allowed"); } CacheCleanerInhibitor inhibitor; ItemRetriever retriever(connection()); retriever.setItemSet(cmd.items().uidSet()); retriever.setRetrieveFullPayload(true); connect(&retriever, &ItemRetriever::itemsRetrieved, this, &Copy::itemsRetrieved); if (!retriever.exec()) { return failureResponse(retriever.lastError()); } return successResponse(); } diff --git a/src/server/handler/move.cpp b/src/server/handler/move.cpp index cd3a5f80c..f83326f9e 100644 --- a/src/server/handler/move.cpp +++ b/src/server/handler/move.cpp @@ -1,162 +1,162 @@ /* Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "move.h" #include "connection.h" #include "handlerhelper.h" #include "cachecleaner.h" #include "storage/datastore.h" #include "storage/itemretriever.h" #include "storage/itemqueryhelper.h" #include "storage/selectquerybuilder.h" #include "storage/transaction.h" #include "storage/collectionqueryhelper.h" using namespace Akonadi; using namespace Akonadi::Server; void Move::itemsRetrieved(const QList &ids) { DataStore *store = connection()->storageBackend(); Transaction transaction(store, QStringLiteral("MOVE")); SelectQueryBuilder qb; ItemQueryHelper::itemSetToQuery(ImapSet(ids), qb); qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::NotEquals, mDestination.id()); - const QDateTime mtime = QDateTime::currentDateTime(); + const QDateTime mtime = QDateTime::currentDateTimeUtc(); if (!qb.exec()) { failureResponse("Unable to execute query"); return; } const QVector items = qb.result(); if (items.isEmpty()) { return; } // Split the list by source collection QMap toMove; QMap sources; ImapSet toMoveIds; Q_FOREACH (/*sic!*/ PimItem item, items) { //krazy:exclude=foreach if (!item.isValid()) { failureResponse("Invalid item in result set!?"); return; } const Collection source = item.collection(); if (!source.isValid()) { failureResponse("Item without collection found!?"); return; } if (!sources.contains(source.id())) { sources.insert(source.id(), source); } Q_ASSERT(item.collectionId() != mDestination.id()); item.setCollectionId(mDestination.id()); item.setAtime(mtime); item.setDatetime(mtime); // if the resource moved itself, we assume it did so because the change happend in the backend if (connection()->context()->resource().id() != mDestination.resourceId()) { item.setDirty(true); } if (!item.update()) { failureResponse("Unable to update item"); return; } toMove.insertMulti(source.id(), item); toMoveIds.add(QVector{ item.id() }); } if (!transaction.commit()) { failureResponse("Unable to commit transaction."); return; } // Batch-reset RID // The item should have an empty RID in the destination collection to avoid // RID conflicts with existing items (see T3904 in Phab). QueryBuilder qb2(PimItem::tableName(), QueryBuilder::Update); qb2.setColumnValue(PimItem::remoteIdColumn(), QString()); ItemQueryHelper::itemSetToQuery(toMoveIds, connection()->context(), qb2); if (!qb2.exec()) { failureResponse("Unable to update RID"); return; } // Emit notification for each source collection separately Collection source; PimItem::List itemsToMove; for (auto it = toMove.cbegin(), end = toMove.cend(); it != end; ++it) { if (source.id() != it.key()) { if (!itemsToMove.isEmpty()) { store->notificationCollector()->itemsMoved(itemsToMove, source, mDestination); } source = sources.value(it.key()); itemsToMove.clear(); } itemsToMove.push_back(*it); } if (!itemsToMove.isEmpty()) { store->notificationCollector()->itemsMoved(itemsToMove, source, mDestination); } } bool Move::parseStream() { const auto &cmd = Protocol::cmdCast(m_command); mDestination = HandlerHelper::collectionFromScope(cmd.destination(), connection()); if (mDestination.isVirtual()) { return failureResponse("Moving items into virtual collection is not allowed"); } if (!mDestination.isValid()) { return failureResponse("Invalid destination collection"); } connection()->context()->setScopeContext(cmd.itemsContext()); if (cmd.items().scope() == Scope::Rid) { if (!connection()->context()->collection().isValid()) { return failureResponse("RID move requires valid source collection"); } } CacheCleanerInhibitor inhibitor; // make sure all the items we want to move are in the cache ItemRetriever retriever(connection()); retriever.setScope(cmd.items()); retriever.setRetrieveFullPayload(true); connect(&retriever, &ItemRetriever::itemsRetrieved, this, &Move::itemsRetrieved); if (!retriever.exec()) { return failureResponse(retriever.lastError()); } return successResponse(); } diff --git a/src/server/storage/akonadidb.xml b/src/server/storage/akonadidb.xml index 36c9414d5..a96ad23b8 100644 --- a/src/server/storage/akonadidb.xml +++ b/src/server/storage/akonadidb.xml @@ -1,248 +1,248 @@ Contains the schema version of the database.
This meta data is stored inside akonadi to provide fast access.
- + create/modified time - + read access time Indicates that this item has unsaved changes.
This meta data is stored inside akonadi to provide fast access.
Table containing item part types. Part name, without namespace. Part namespace.
Specifies allowed MimeType for a Collection Used to associate items with search folders.
diff --git a/src/server/storage/datastore.cpp b/src/server/storage/datastore.cpp index 6427df0d2..d0a5dff98 100644 --- a/src/server/storage/datastore.cpp +++ b/src/server/storage/datastore.cpp @@ -1,1479 +1,1479 @@ /*************************************************************************** * Copyright (C) 2006 by Andreas Gungl * * Copyright (C) 2007 by Robert Zwerus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "datastore.h" #include "akonadi.h" #include "dbconfig.h" #include "dbinitializer.h" #include "dbupdater.h" #include "notificationmanager.h" #include "tracer.h" #include "transaction.h" #include "selectquerybuilder.h" #include "handlerhelper.h" #include "countquerybuilder.h" #include "parthelper.h" #include "handler.h" #include "collectionqueryhelper.h" #include "akonadischema.h" #include "parttypehelper.h" #include "querycache.h" #include "queryhelper.h" #include "akonadiserver_debug.h" #include "storagedebugger.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace Akonadi::Server; static QMutex sTransactionMutex; bool DataStore::s_hasForeignKeyConstraints = false; QThreadStorage DataStore::sInstances; #define TRANSACTION_MUTEX_LOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.lock() #define TRANSACTION_MUTEX_UNLOCK if ( DbType::isSystemSQLite( m_database ) ) sTransactionMutex.unlock() #define setBoolPtr(ptr, val) \ { \ if ((ptr)) { \ *(ptr) = (val); \ } \ } /*************************************************************************** * DataStore * ***************************************************************************/ DataStore::DataStore() : QObject() , m_dbOpened(false) , m_transactionLevel(0) , mNotificationCollector(nullptr) , m_keepAliveTimer(nullptr) { notificationCollector(); if (DbConfig::configuredDatabase()->driverName() == QLatin1String("QMYSQL")) { // Send a dummy query to MySQL every 1 hour to keep the connection alive, // otherwise MySQL just drops the connection and our subsequent queries fail // without properly reporting the error m_keepAliveTimer = new QTimer(this); m_keepAliveTimer->setInterval(3600 * 1000); QObject::connect(m_keepAliveTimer, &QTimer::timeout, this, &DataStore::sendKeepAliveQuery); m_keepAliveTimer->start(); } } DataStore::~DataStore() { if (m_dbOpened) { close(); } } void DataStore::open() { m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast(QThread::currentThread())); Q_ASSERT(!QSqlDatabase::contains(m_connectionName)); m_database = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), m_connectionName); DbConfig::configuredDatabase()->apply(m_database); if (!m_database.isValid()) { m_dbOpened = false; return; } m_dbOpened = m_database.open(); if (!m_dbOpened) { debugLastDbError("Cannot open database."); } else { qCDebug(AKONADISERVER_LOG) << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName(); } StorageDebugger::instance()->addConnection(reinterpret_cast(this), QThread::currentThread()->objectName()); connect(QThread::currentThread(), &QThread::objectNameChanged, this, [this](const QString &name) { if (!name.isEmpty()) { StorageDebugger::instance()->changeConnection(reinterpret_cast(this), name); } }); DbConfig::configuredDatabase()->initSession(m_database); } QSqlDatabase DataStore::database() { if (!m_dbOpened) { open(); } return m_database; } void DataStore::close() { if (m_keepAliveTimer) { m_keepAliveTimer->stop(); } if (!m_dbOpened) { return; } if (inTransaction()) { // By setting m_transactionLevel to '1' here, we skip all nested transactions // and rollback the outermost transaction. m_transactionLevel = 1; rollbackTransaction(); } QueryCache::clear(); m_database.close(); m_database = QSqlDatabase(); m_transactionQueries.clear(); QSqlDatabase::removeDatabase(m_connectionName); StorageDebugger::instance()->removeConnection(reinterpret_cast(this)); m_dbOpened = false; } bool DataStore::init() { Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); AkonadiSchema schema; DbInitializer::Ptr initializer = DbInitializer::createInstance(database(), &schema); if (!initializer->run()) { qCCritical(AKONADISERVER_LOG) << initializer->errorMsg(); return false; } s_hasForeignKeyConstraints = initializer->hasForeignKeyConstraints(); if (QFile::exists(QStringLiteral(":dbupdate.xml"))) { DbUpdater updater(database(), QStringLiteral(":dbupdate.xml")); if (!updater.run()) { return false; } } else { qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates"; } if (!initializer->updateIndexesAndConstraints()) { qCCritical(AKONADISERVER_LOG) << initializer->errorMsg(); return false; } // enable caching for some tables MimeType::enableCache(true); Flag::enableCache(true); Resource::enableCache(true); Collection::enableCache(true); PartType::enableCache(true); return true; } NotificationCollector *DataStore::notificationCollector() { if (mNotificationCollector == nullptr) { mNotificationCollector = new NotificationCollector(this); NotificationManager *notificationManager = AkonadiServer::instance()->notificationManager(); if (notificationManager) { notificationManager->connectNotificationCollector(notificationCollector()); } } return mNotificationCollector; } DataStore *DataStore::self() { if (!sInstances.hasLocalData()) { sInstances.setLocalData(new DataStore()); } return sInstances.localData(); } bool DataStore::hasDataStore() { return sInstances.hasLocalData(); } /* --- ItemFlags ----------------------------------------------------- */ bool DataStore::setItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, const Collection &col_, bool silent) { QSet removedFlags; QSet addedFlags; QVariantList insIds; QVariantList insFlags; Query::Condition delConds(Query::Or); Collection col = col_; setBoolPtr(flagsChanged, false); for (const PimItem &item : items) { const Flag::List itemFlags = item.flags(); for (const Flag &flag : itemFlags) { if (!flags.contains(flag)) { removedFlags << flag.name().toLatin1(); Query::Condition cond; cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id()); cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id()); delConds.addCondition(cond); } } Q_FOREACH (const Flag &flag, flags) { if (!itemFlags.contains(flag)) { addedFlags << flag.name().toLatin1(); insIds << item.id(); insFlags << flag.id(); } } if (col.id() == -1) { col.setId(item.collectionId()); } else if (col.id() != item.collectionId()) { col.setId(-2); } } if (!removedFlags.empty()) { QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); qb.addCondition(delConds); if (!qb.exec()) { return false; } } if (!addedFlags.empty()) { QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds); qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { return false; } } if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) { mNotificationCollector->itemsFlagsChanged(items, addedFlags, removedFlags, col); } setBoolPtr(flagsChanged, (addedFlags != removedFlags)); return true; } bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet &existing, const Collection &col_, bool silent) { Collection col = col_; QVariantList flagIds; QVariantList appendIds; PimItem::List appendItems; for (const PimItem &item : items) { if (existing.contains(item.id())) { continue; } flagIds << flag.id(); appendIds << item.id(); appendItems << item; if (col.id() == -1) { col.setId(item.collectionId()); } else if (col.id() != item.collectionId()) { col.setId(-2); } } if (appendItems.isEmpty()) { return true; // all items have the desired flags already } QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds); qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb2.query().lastError(); return false; } if (!silent) { mNotificationCollector->itemsFlagsChanged(appendItems, QSet() << flag.name().toLatin1(), QSet(), col); } return true; } bool DataStore::appendItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, bool checkIfExists, const Collection &col, bool silent) { QVariantList itemsIds; itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds.append(item.id()); } setBoolPtr(flagsChanged, false); for (const Flag &flag : flags) { QSet existing; if (checkIfExists) { QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select); Query::Condition cond; cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id()); cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds); qb.addColumn(PimItemFlagRelation::leftColumn()); qb.addCondition(cond); if (!qb.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb.query().lastError(); return false; } QSqlQuery query = qb.query(); if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { //The query size feature is not suppoerted by the sqllite driver if (query.size() == items.count()) { continue; } setBoolPtr(flagsChanged, true); } while (query.next()) { existing << query.value(0).value(); } if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) { if (existing.size() != items.count()) { setBoolPtr(flagsChanged, true); } } } if (!doAppendItemsFlag(items, flag, existing, col, silent)) { return false; } } return true; } bool DataStore::removeItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, const Collection &col_, bool silent) { Collection col = col_; QSet removedFlags; QVariantList itemsIds; QVariantList flagsIds; setBoolPtr(flagsChanged, false); itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds << item.id(); if (col.id() == -1) { col.setId(item.collectionId()); } else if (col.id() != item.collectionId()) { col.setId(-2); } for (int i = 0; i < flags.count(); ++i) { const QByteArray flagName = flags[i].name().toLatin1(); if (!removedFlags.contains(flagName)) { flagsIds << flags[i].id(); removedFlags << flagName; } } } // Delete all given flags from all given items in one go QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); Query::Condition cond(Query::And); cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds); cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds); qb.addCondition(cond); if (!qb.exec()) { return false; } if (qb.query().numRowsAffected() != 0) { setBoolPtr(flagsChanged, true); if (!silent) { mNotificationCollector->itemsFlagsChanged(items, QSet(), removedFlags, col); } } return true; } /* --- ItemTags ----------------------------------------------------- */ bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) { QSet removedTags; QSet addedTags; QVariantList insIds; QVariantList insTags; Query::Condition delConds(Query::Or); setBoolPtr(tagsChanged, false); for (const PimItem &item : items) { const Tag::List itemTags = item.tags(); for (const Tag &tag : itemTags) { if (!tags.contains(tag)) { // Remove tags from items that had it set removedTags << tag.id(); Query::Condition cond; cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id()); cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id()); delConds.addCondition(cond); } } for (const Tag &tag : tags) { if (!itemTags.contains(tag)) { // Add tags to items that did not have the tag addedTags << tag.id(); insIds << item.id(); insTags << tag.id(); } } } if (!removedTags.empty()) { QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); qb.addCondition(delConds); if (!qb.exec()) { return false; } } if (!addedTags.empty()) { QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds); qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { return false; } } if (!silent && (!addedTags.empty() || !removedTags.empty())) { mNotificationCollector->itemsTagsChanged(items, addedTags, removedTags); } setBoolPtr(tagsChanged, (addedTags != removedTags)); return true; } bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet &existing, const Collection &col, bool silent) { QVariantList tagIds; QVariantList appendIds; PimItem::List appendItems; for (const PimItem &item : items) { if (existing.contains(item.id())) { continue; } tagIds << tag.id(); appendIds << item.id(); appendItems << item; } if (appendItems.isEmpty()) { return true; // all items have the desired tags already } QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds); qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds); qb2.setIdentificationColumn(QString()); if (!qb2.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb2.query().lastError(); return false; } if (!silent) { mNotificationCollector->itemsTagsChanged(appendItems, QSet() << tag.id(), QSet(), col); } return true; } bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool checkIfExists, const Collection &col, bool silent) { QVariantList itemsIds; itemsIds.reserve(items.count()); for (const PimItem &item : items) { itemsIds.append(item.id()); } setBoolPtr(tagsChanged, false); for (const Tag &tag : tags) { QSet existing; if (checkIfExists) { QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select); Query::Condition cond; cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id()); cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds); qb.addColumn(PimItemTagRelation::leftColumn()); qb.addCondition(cond); if (!qb.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query:" << qb.query().lastError(); return false; } QSqlQuery query = qb.query(); if (query.size() == items.count()) { continue; } setBoolPtr(tagsChanged, true); while (query.next()) { existing << query.value(0).value(); } } if (!doAppendItemsTag(items, tag, existing, col, silent)) { return false; } } return true; } bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) { QSet removedTags; QVariantList itemsIds; QVariantList tagsIds; setBoolPtr(tagsChanged, false); itemsIds.reserve(items.count()); Q_FOREACH (const PimItem &item, items) { itemsIds << item.id(); for (int i = 0; i < tags.count(); ++i) { const qint64 tagId = tags[i].id(); if (!removedTags.contains(tagId)) { tagsIds << tagId; removedTags << tagId; } } } // Delete all given tags from all given items in one go QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); Query::Condition cond(Query::And); cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds); cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds); qb.addCondition(cond); if (!qb.exec()) { return false; } if (qb.query().numRowsAffected() != 0) { setBoolPtr(tagsChanged, true); if (!silent) { mNotificationCollector->itemsTagsChanged(items, QSet(), removedTags); } } return true; } bool DataStore::removeTags(const Tag::List &tags, bool silent) { // Currently the "silent" argument is only for API symmetry Q_UNUSED(silent); QVariantList removedTagsIds; QSet removedTags; removedTagsIds.reserve(tags.count()); removedTags.reserve(tags.count()); Q_FOREACH (const Tag &tag, tags) { removedTagsIds << tag.id(); removedTags << tag.id(); } // Get all PIM items that we will untag SelectQueryBuilder itemsQuery; itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName()); itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds); if (!itemsQuery.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << itemsQuery.query().lastError(); return false; } const PimItem::List items = itemsQuery.result(); if (!items.isEmpty()) { DataStore::self()->notificationCollector()->itemsTagsChanged(items, QSet(), removedTags); } Q_FOREACH (const Tag &tag, tags) { // Emit special tagRemoved notification for each resource that owns the tag QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select); qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName()); qb.addColumn(Resource::nameFullColumnName()); qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id()); if (!qb.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << qb.query().lastError(); return false; } // Emit specialized notifications for each resource QSqlQuery query = qb.query(); while (query.next()) { const QString rid = query.value(0).toString(); const QByteArray resource = query.value(1).toByteArray(); DataStore::self()->notificationCollector()->tagRemoved(tag, resource, rid); } // And one for clients - without RID DataStore::self()->notificationCollector()->tagRemoved(tag, QByteArray(), QString()); } // Just remove the tags, table constraints will take care of the rest QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete); qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds); if (!qb.exec()) { qCDebug(AKONADISERVER_LOG) << "Failed to execute query: " << itemsQuery.query().lastError(); return false; } return true; } /* --- ItemParts ----------------------------------------------------- */ bool DataStore::removeItemParts(const PimItem &item, const QSet &parts) { SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); qb.addCondition(PartTypeHelper::conditionFromFqNames(parts)); qb.exec(); const Part::List existingParts = qb.result(); for (Part part : qAsConst(existingParts)) { //krazy:exclude=foreach if (!PartHelper::remove(&part)) { return false; } } mNotificationCollector->itemChanged(item, parts); return true; } bool DataStore::invalidateItemCache(const PimItem &item) { // find all payload item parts SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); if (!qb.exec()) { return false; } const Part::List parts = qb.result(); // clear data field for (Part part : parts) { if (!PartHelper::truncate(part)) { return false; } } return true; } /* --- Collection ------------------------------------------------------ */ bool DataStore::appendCollection(Collection &collection) { // no need to check for already existing collection with the same name, // a unique index on parent + name prevents that in the database if (!collection.insert()) { return false; } mNotificationCollector->collectionAdded(collection); return true; } bool DataStore::cleanupCollection(Collection &collection) { if (!s_hasForeignKeyConstraints) { return cleanupCollection_slow(collection); } // db will do most of the work for us, we just deal with notifications and external payload parts here Q_ASSERT(s_hasForeignKeyConstraints); // collect item deletion notifications const PimItem::List items = collection.items(); const QByteArray resource = collection.resource().name().toLatin1(); // generate the notification before actually removing the data // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though mNotificationCollector->itemsRemoved(items, collection, resource); // remove all external payload parts QueryBuilder qb(Part::tableName(), QueryBuilder::Select); qb.addColumn(Part::dataFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id()); qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); if (!qb.exec()) { return false; } try { while (qb.query().next()) { ExternalPartStorage::self()->removePartFile( ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray())); } } catch (const PartHelperException &e) { qCDebug(AKONADISERVER_LOG) << e.what(); return false; } // delete the collection itself, referential actions will do the rest mNotificationCollector->collectionRemoved(collection); return collection.remove(); } bool DataStore::cleanupCollection_slow(Collection &collection) { Q_ASSERT(!s_hasForeignKeyConstraints); // delete the content const PimItem::List items = collection.items(); const QByteArray resource = collection.resource().name().toLatin1(); mNotificationCollector->itemsRemoved(items, collection, resource); for (const PimItem &item : items) { if (!item.clearFlags()) { // TODO: move out of loop and use only a single query return false; } if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { // TODO: reduce to single query return false; } if (!PimItem::remove(PimItem::idColumn(), item.id())) { // TODO: move into single query return false; } if (!Entity::clearRelation(item.id(), Entity::Right)) { // TODO: move into single query return false; } } // delete collection mimetypes collection.clearMimeTypes(); Collection::clearPimItems(collection.id()); // delete attributes Q_FOREACH (CollectionAttribute attr, collection.attributes()) { //krazy:exclude=foreach if (!attr.remove()) { return false; } } // delete the collection itself mNotificationCollector->collectionRemoved(collection); return collection.remove(); } static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId) { Transaction transaction(DataStore::self(), QStringLiteral("RECURSIVE SET RESOURCEID")); QueryBuilder qb(Collection::tableName(), QueryBuilder::Update); qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id()); qb.setColumnValue(Collection::resourceIdColumn(), resourceId); qb.setColumnValue(Collection::remoteIdColumn(), QVariant()); qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant()); if (!qb.exec()) { return false; } // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc) // as well as mark the items dirty to prevent cache purging before they have been written back qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update); qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id()); qb.setColumnValue(PimItem::remoteIdColumn(), QVariant()); qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant()); - const QDateTime now = QDateTime::currentDateTime(); + const QDateTime now = QDateTime::currentDateTimeUtc(); qb.setColumnValue(PimItem::datetimeColumn(), now); qb.setColumnValue(PimItem::atimeColumn(), now); qb.setColumnValue(PimItem::dirtyColumn(), true); if (!qb.exec()) { return false; } transaction.commit(); Q_FOREACH (const Collection &col, collection.children()) { if (!recursiveSetResourceId(col, resourceId)) { return false; } } return true; } bool DataStore::moveCollection(Collection &collection, const Collection &newParent) { if (collection.parentId() == newParent.id()) { return true; } if (!m_dbOpened || !newParent.isValid()) { return false; } const QByteArray oldResource = collection.resource().name().toLatin1(); int resourceId = collection.resourceId(); const Collection source = collection.parent(); if (newParent.id() > 0) { // not root resourceId = newParent.resourceId(); } if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) { return false; } collection.setParentId(newParent.id()); if (collection.resourceId() != resourceId) { collection.setResourceId(resourceId); collection.setRemoteId(QString()); collection.setRemoteRevision(QString()); if (!recursiveSetResourceId(collection, resourceId)) { return false; } } if (!collection.update()) { return false; } mNotificationCollector->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1()); return true; } bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes) { if (mimeTypes.isEmpty()) { return true; } for (const QString &mimeType : mimeTypes) { const auto &mt = MimeType::retrieveByNameOrCreate(mimeType); if (!mt.isValid()) { return false; } if (!Collection::addMimeType(collectionId, mt.id())) { return false; } } return true; } void DataStore::activeCachePolicy(Collection &col) { if (!col.cachePolicyInherit()) { return; } Collection parent = col; while (parent.parentId() != 0) { parent = parent.parent(); if (!parent.cachePolicyInherit()) { col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval()); col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout()); col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand()); col.setCachePolicyLocalParts(parent.cachePolicyLocalParts()); return; } } // ### system default col.setCachePolicyCheckInterval(-1); col.setCachePolicyCacheTimeout(-1); col.setCachePolicySyncOnDemand(false); col.setCachePolicyLocalParts(QStringLiteral("ALL")); } QVector DataStore::virtualCollections(const PimItem &item) { SelectQueryBuilder qb; qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id()); if (!qb.exec()) { qCDebug(AKONADISERVER_LOG) << "Error during selection of records from table CollectionPimItemRelation" << qb.query().lastError().text(); return QVector(); } return qb.result(); } QMap > DataStore::virtualCollections(const PimItem::List &items) { QueryBuilder qb(CollectionPimItemRelation::tableName(), QueryBuilder::Select); qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), CollectionPimItemRelation::rightFullColumnName()); qb.addColumn(Collection::idFullColumnName()); qb.addColumns(QStringList() << PimItem::idFullColumnName() << PimItem::remoteIdFullColumnName() << PimItem::remoteRevisionFullColumnName() << PimItem::mimeTypeIdFullColumnName()); qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending); if (items.count() == 1) { qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, items.first().id()); } else { QVariantList ids; ids.reserve(items.count()); Q_FOREACH (const PimItem &item, items) { ids << item.id(); } qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::In, ids); } if (!qb.exec()) { qCDebug(AKONADISERVER_LOG) << "Error during selection of records from table CollectionPimItemRelation" << qb.query().lastError().text(); return QMap >(); } QSqlQuery query = qb.query(); QMap > map; query.next(); while (query.isValid()) { const qlonglong collectionId = query.value(0).toLongLong(); QList &pimItems = map[collectionId]; do { PimItem item; item.setId(query.value(1).toLongLong()); item.setRemoteId(query.value(2).toString()); item.setRemoteRevision(query.value(3).toString()); item.setMimeTypeId(query.value(4).toLongLong()); pimItems << item; } while (query.next() && query.value(0).toLongLong() == collectionId); } return map; } /* --- PimItem ------------------------------------------------------- */ bool DataStore::appendPimItem(QVector &parts, const QVector &flags, const MimeType &mimetype, const Collection &collection, const QDateTime &dateTime, const QString &remote_id, const QString &remoteRevision, const QString &gid, PimItem &pimItem) { pimItem.setMimeTypeId(mimetype.id()); pimItem.setCollectionId(collection.id()); if (dateTime.isValid()) { pimItem.setDatetime(dateTime); } if (remote_id.isEmpty()) { // from application pimItem.setDirty(true); } else { // from resource pimItem.setRemoteId(remote_id); pimItem.setDirty(false); } pimItem.setRemoteRevision(remoteRevision); pimItem.setGid(gid); - pimItem.setAtime(QDateTime::currentDateTime()); + pimItem.setAtime(QDateTime::currentDateTimeUtc()); if (!pimItem.insert()) { return false; } // insert every part if (!parts.isEmpty()) { //don't use foreach, the caller depends on knowing the part has changed, see the Append handler for (QVector::iterator it = parts.begin(); it != parts.end(); ++it) { (*it).setPimItemId(pimItem.id()); if ((*it).datasize() < (*it).data().size()) { (*it).setDatasize((*it).data().size()); } // qCDebug(AKONADISERVER_LOG) << "Insert from DataStore::appendPimItem"; if (!PartHelper::insert(&(*it))) { return false; } } } bool seen = false; Q_FOREACH (const Flag &flag, flags) { seen |= (flag.name() == QLatin1String(AKONADI_FLAG_SEEN) || flag.name() == QLatin1String(AKONADI_FLAG_IGNORED)); if (!pimItem.addFlag(flag)) { return false; } } // qCDebug(AKONADISERVER_LOG) << "appendPimItem: " << pimItem; mNotificationCollector->itemAdded(pimItem, seen, collection); return true; } bool DataStore::unhidePimItem(PimItem &pimItem) { if (!m_dbOpened) { return false; } qCDebug(AKONADISERVER_LOG) << "DataStore::unhidePimItem(" << pimItem << ")"; // FIXME: This is inefficient. Using a bit on the PimItemTable record would probably be some orders of magnitude faster... return removeItemParts(pimItem, { AKONADI_ATTRIBUTE_HIDDEN }); } bool DataStore::unhideAllPimItems() { if (!m_dbOpened) { return false; } qCDebug(AKONADISERVER_LOG) << "DataStore::unhideAllPimItems()"; try { return PartHelper::remove(Part::partTypeIdFullColumnName(), PartTypeHelper::fromFqName(QStringLiteral("ATR"), QStringLiteral("HIDDEN")).id()); } catch (...) { } // we can live with this failing return false; } bool DataStore::cleanupPimItems(const PimItem::List &items) { // generate relation removed notifications for (const PimItem &item : items) { SelectQueryBuilder relationQuery; relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, item.id()); relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, item.id()); relationQuery.setSubQueryMode(Query::Or); if (!relationQuery.exec()) { throw HandlerException("Failed to obtain relations"); } const Relation::List relations = relationQuery.result(); for (const Relation &relation : relations) { DataStore::self()->notificationCollector()->relationRemoved(relation); } } // generate the notification before actually removing the data mNotificationCollector->itemsRemoved(items); // FIXME: Create a single query to do this Q_FOREACH (const PimItem &item, items) { if (!item.clearFlags()) { return false; } if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { return false; } if (!PimItem::remove(PimItem::idColumn(), item.id())) { return false; } if (!Entity::clearRelation(item.id(), Entity::Right)) { return false; } } return true; } bool DataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value) { SelectQueryBuilder qb; qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); if (!qb.exec()) { return false; } if (!qb.result().isEmpty()) { qCDebug(AKONADISERVER_LOG) << "Attribute" << key << "already exists for collection" << col.id(); return false; } CollectionAttribute attr; attr.setCollectionId(col.id()); attr.setType(key); attr.setValue(value); if (!attr.insert()) { return false; } mNotificationCollector->collectionChanged(col, QList() << key); return true; } bool DataStore::removeCollectionAttribute(const Collection &col, const QByteArray &key) { SelectQueryBuilder qb; qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); if (!qb.exec()) { throw HandlerException("Unable to query for collection attribute"); } const QVector result = qb.result(); for (CollectionAttribute attr : result) { if (!attr.remove()) { throw HandlerException("Unable to remove collection attribute"); } } if (!result.isEmpty()) { mNotificationCollector->collectionChanged(col, QList() << key); return true; } return false; } void DataStore::debugLastDbError(const char *actionDescription) const { qCCritical(AKONADISERVER_LOG) << "Database error:" << actionDescription; qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText(); qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText(); Tracer::self()->error("DataStore (Database Error)", QStringLiteral("%1\nDriver said: %2\nDatabase said:%3") .arg(QString::fromLatin1(actionDescription), m_database.lastError().driverText(), m_database.lastError().databaseText())); } void DataStore::debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const { qCCritical(AKONADISERVER_LOG) << "Query error:" << actionDescription; qCCritical(AKONADISERVER_LOG) << " Last error message:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText(); qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText(); Tracer::self()->error("DataStore (Database Query Error)", QStringLiteral("%1: %2") .arg(QString::fromLatin1(actionDescription), query.lastError().text())); } // static QString DataStore::dateTimeFromQDateTime(const QDateTime &dateTime) { QDateTime utcDateTime = dateTime; if (utcDateTime.timeSpec() != Qt::UTC) { utcDateTime.toUTC(); } return utcDateTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")); } // static QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime) { return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss")); } void DataStore::addQueryToTransaction(const QSqlQuery &query, bool isBatch) { // This is used for replaying deadlocked transactions, so only record queries // for backends that support concurrent transactions. if (!inTransaction() || DbType::isSystemSQLite(m_database)) { return; } m_transactionQueries.append(qMakePair(query, isBatch)); } QSqlQuery DataStore::retryLastTransaction(bool rollbackFirst) { if (!inTransaction() || DbType::isSystemSQLite(m_database)) { return QSqlQuery(); } if (rollbackFirst) { // In some cases the SQL database won't rollback the failed transaction, so // we need to do it manually m_database.driver()->rollbackTransaction(); } // The database has rolled back the actual transaction, so reset the counter // to 0 and start a new one in beginTransaction(). Then restore the level // because this has to be completely transparent to the original caller const int oldTransactionLevel = m_transactionLevel; m_transactionLevel = 0; if (!beginTransaction(QStringLiteral("RETRY LAST TRX"))) { m_transactionLevel = oldTransactionLevel; return QSqlQuery(); } m_transactionLevel = oldTransactionLevel; typedef QPair QueryBoolPair; QMutableVectorIterator iter(m_transactionQueries); while (iter.hasNext()) { iter.next(); QSqlQuery query = iter.value().first; const bool isBatch = iter.value().second; // Make sure the query is ready to be executed again if (query.isActive()) { query.finish(); } bool res = false; if (isBatch) { // QSqlQuery::execBatch() does not reset lastError(), so for the sake // of transparency (make it look to the caller like if the query was // successful the first time), we create a copy of the original query, // which has lastError empty. QSqlQuery copiedQuery(m_database); copiedQuery.prepare(query.executedQuery()); const QMap boundValues = query.boundValues(); int i = 0; for (const QVariant &value : boundValues) { copiedQuery.bindValue(i, value); ++i; } query = copiedQuery; res = query.execBatch(); } else { res = query.exec(); } if (!res) { // Don't do another deadlock detection here, just give up. qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR:"; qCCritical(AKONADISERVER_LOG) << " Error code:" << query.lastError().nativeErrorCode(); qCCritical(AKONADISERVER_LOG) << " DB error: " << query.lastError().databaseText(); qCCritical(AKONADISERVER_LOG) << " Error text:" << query.lastError().text(); qCCritical(AKONADISERVER_LOG) << " Query:" << query.executedQuery(); // Return the last query, because that's what caller expects to retrieve // from QueryBuilder. It is in error state anyway. return m_transactionQueries.last().first; } // Update the query in the list iter.setValue(qMakePair(query, isBatch)); } return m_transactionQueries.last().first; } bool DataStore::beginTransaction(const QString &name) { if (!m_dbOpened) { return false; } if (m_transactionLevel == 0) { QElapsedTimer timer; timer.start(); TRANSACTION_MUTEX_LOCK; if (DbType::type(m_database) == DbType::Sqlite) { m_database.exec(QStringLiteral("BEGIN IMMEDIATE TRANSACTION")); StorageDebugger::instance()->addTransaction(reinterpret_cast(this), name, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { debugLastDbError("DataStore::beginTransaction (SQLITE)"); TRANSACTION_MUTEX_UNLOCK; return false; } } else { m_database.driver()->beginTransaction(); StorageDebugger::instance()->addTransaction(reinterpret_cast(this), name, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { debugLastDbError("DataStore::beginTransaction"); TRANSACTION_MUTEX_UNLOCK; return false; } } if (DbType::type(m_database) == DbType::PostgreSQL) { // Make constraints check deferred in PostgreSQL. Allows for // INSERT INTO mimetypetable (name) VALUES ('foo') RETURNING id; // INSERT INTO collectionmimetyperelation (collection_id, mimetype_id) VALUES (x, y) // where "y" refers to the newly inserted mimetype m_database.exec(QStringLiteral("SET CONSTRAINTS ALL DEFERRED")); } } ++m_transactionLevel; return true; } bool DataStore::rollbackTransaction() { if (!m_dbOpened) { return false; } if (m_transactionLevel == 0) { qCWarning(AKONADISERVER_LOG) << "DataStore::rollbackTransaction(): No transaction in progress!"; return false; } --m_transactionLevel; if (m_transactionLevel == 0) { QSqlDriver *driver = m_database.driver(); Q_EMIT transactionRolledBack(); QElapsedTimer timer; timer.start(); driver->rollbackTransaction(); StorageDebugger::instance()->removeTransaction(reinterpret_cast(this), false, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { TRANSACTION_MUTEX_UNLOCK; debugLastDbError("DataStore::rollbackTransaction"); return false; } TRANSACTION_MUTEX_UNLOCK; m_transactionQueries.clear(); } return true; } bool DataStore::commitTransaction() { if (!m_dbOpened) { return false; } if (m_transactionLevel == 0) { qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): No transaction in progress!"; return false; } if (m_transactionLevel == 1) { QSqlDriver *driver = m_database.driver(); QElapsedTimer timer; timer.start(); driver->commitTransaction(); StorageDebugger::instance()->removeTransaction(reinterpret_cast(this), true, timer.elapsed(), m_database.lastError().text()); if (m_database.lastError().isValid()) { debugLastDbError("DataStore::commitTransaction"); rollbackTransaction(); return false; } else { TRANSACTION_MUTEX_UNLOCK; Q_EMIT transactionCommitted(); } m_transactionQueries.clear(); } m_transactionLevel--; return true; } bool DataStore::inTransaction() const { return m_transactionLevel > 0; } void DataStore::sendKeepAliveQuery() { if (m_database.isOpen()) { QSqlQuery query(m_database); query.exec(QStringLiteral("SELECT 1")); } } diff --git a/src/server/storage/dbinitializer.cpp b/src/server/storage/dbinitializer.cpp index e2a4a98ab..92b06bdc4 100644 --- a/src/server/storage/dbinitializer.cpp +++ b/src/server/storage/dbinitializer.cpp @@ -1,417 +1,417 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * Copyright (C) 2012 by Volker Krause * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "dbinitializer.h" #include "dbinitializer_p.h" #include "querybuilder.h" #include "dbexception.h" #include "schema.h" #include "entities.h" #include "akonadiserver_debug.h" #include #include #include #include #include using namespace Akonadi::Server; DbInitializer::Ptr DbInitializer::createInstance(const QSqlDatabase &database, Schema *schema) { DbInitializer::Ptr i; switch (DbType::type(database)) { case DbType::MySQL: i.reset(new DbInitializerMySql(database)); break; case DbType::Sqlite: i.reset(new DbInitializerSqlite(database)); break; case DbType::PostgreSQL: i.reset(new DbInitializerPostgreSql(database)); break; case DbType::Unknown: qCCritical(AKONADISERVER_LOG) << database.driverName() << "backend not supported"; break; } i->mSchema = schema; return i; } DbInitializer::DbInitializer(const QSqlDatabase &database) : mDatabase(database) , mSchema(nullptr) , mTestInterface(nullptr) , m_noForeignKeyContraints(false) { m_introspector = DbIntrospector::createInstance(mDatabase); } DbInitializer::~DbInitializer() { } bool DbInitializer::run() { try { qCDebug(AKONADISERVER_LOG) << "DbInitializer::run()"; Q_FOREACH (const TableDescription &table, mSchema->tables()) { if (!checkTable(table)) { return false; } } Q_FOREACH (const RelationDescription &relation, mSchema->relations()) { if (!checkRelation(relation)) { return false; } } #ifndef DBINITIALIZER_UNITTEST // Now finally check and set the generation identifier if necessary SchemaVersion version = SchemaVersion::retrieveAll().first(); if (version.generation() == 0) { version.setGeneration(QDateTime::currentDateTimeUtc().toTime_t()); version.update(); qCDebug(AKONADISERVER_LOG) << "Generation:" << version.generation(); } #endif qCDebug(AKONADISERVER_LOG) << "DbInitializer::run() done"; return true; } catch (const DbException &e) { mErrorMsg = QString::fromUtf8(e.what()); } return false; } bool DbInitializer::checkTable(const TableDescription &tableDescription) { qCDebug(AKONADISERVER_LOG) << "checking table " << tableDescription.name; if (!m_introspector->hasTable(tableDescription.name)) { // Get the CREATE TABLE statement for the specific SQL dialect const QString createTableStatement = buildCreateTableStatement(tableDescription); qCDebug(AKONADISERVER_LOG) << createTableStatement; execQuery(createTableStatement); } else { // Check for every column whether it exists, and add the missing ones Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { if (!m_introspector->hasColumn(tableDescription.name, columnDescription.name)) { // Don't add the column on update, DbUpdater will add it if (columnDescription.noUpdate) { continue; } // Get the ADD COLUMN statement for the specific SQL dialect const QString statement = buildAddColumnStatement(tableDescription, columnDescription); qCDebug(AKONADISERVER_LOG) << statement; execQuery(statement); } } // NOTE: we do intentionally not delete any columns here, we defer that to the updater, // very likely previous columns contain data that needs to be moved to a new column first. } // Add initial data if table is empty if (tableDescription.data.isEmpty()) { return true; } if (m_introspector->isTableEmpty(tableDescription.name)) { Q_FOREACH (const DataDescription &dataDescription, tableDescription.data) { // Get the INSERT VALUES statement for the specific SQL dialect const QString statement = buildInsertValuesStatement(tableDescription, dataDescription); qCDebug(AKONADISERVER_LOG) << statement; execQuery(statement); } } return true; } void DbInitializer::checkForeignKeys(const TableDescription &tableDescription) { try { const QVector existingForeignKeys = m_introspector->foreignKeyConstraints(tableDescription.name); Q_FOREACH (const ColumnDescription &column, tableDescription.columns) { DbIntrospector::ForeignKey existingForeignKey; Q_FOREACH (const DbIntrospector::ForeignKey &fk, existingForeignKeys) { if (QString::compare(fk.column, column.name, Qt::CaseInsensitive) == 0) { existingForeignKey = fk; break; } } if (!column.refTable.isEmpty() && !column.refColumn.isEmpty()) { if (!existingForeignKey.column.isEmpty()) { // there's a constraint on this column, check if it's the correct one if (QString::compare(existingForeignKey.refTable, column.refTable + QLatin1Literal("table"), Qt::CaseInsensitive) == 0 && QString::compare(existingForeignKey.refColumn, column.refColumn, Qt::CaseInsensitive) == 0 && QString::compare(existingForeignKey.onUpdate, referentialActionToString(column.onUpdate), Qt::CaseInsensitive) == 0 && QString::compare(existingForeignKey.onDelete, referentialActionToString(column.onDelete), Qt::CaseInsensitive) == 0) { continue; // all good } const QString statement = buildRemoveForeignKeyConstraintStatement(existingForeignKey, tableDescription); if (!statement.isEmpty()) { qCDebug(AKONADISERVER_LOG) << "Found existing foreign constraint that doesn't match the schema:" << existingForeignKey.name << existingForeignKey.column << existingForeignKey.refTable << existingForeignKey.refColumn; m_removedForeignKeys << statement; } } const QString statement = buildAddForeignKeyConstraintStatement(tableDescription, column); if (statement.isEmpty()) { // not supported m_noForeignKeyContraints = true; return; } m_pendingForeignKeys << statement; } else if (!existingForeignKey.column.isEmpty()) { // constraint exists but we don't want one here const QString statement = buildRemoveForeignKeyConstraintStatement(existingForeignKey, tableDescription); if (!statement.isEmpty()) { qCDebug(AKONADISERVER_LOG) << "Found unexpected foreign key constraint:" << existingForeignKey.name << existingForeignKey.column << existingForeignKey.refTable << existingForeignKey.refColumn; m_removedForeignKeys << statement; } } } } catch (const DbException &e) { qCDebug(AKONADISERVER_LOG) << "Fixing foreign key constraints failed:" << e.what(); // we ignore this since foreign keys are only used for optimizations (not all backends support them anyway) m_noForeignKeyContraints = true; } } void DbInitializer::checkIndexes(const TableDescription &tableDescription) { // Add indices Q_FOREACH (const IndexDescription &indexDescription, tableDescription.indexes) { // sqlite3 needs unique index identifiers per db const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name); if (!m_introspector->hasIndex(tableDescription.name, indexName)) { // Get the CREATE INDEX statement for the specific SQL dialect m_pendingIndexes << buildCreateIndexStatement(tableDescription, indexDescription); } } } bool DbInitializer::checkRelation(const RelationDescription &relationDescription) { return checkTable(RelationTableDescription(relationDescription)); } QString DbInitializer::errorMsg() const { return mErrorMsg; } bool DbInitializer::hasForeignKeyConstraints() const { return !m_noForeignKeyContraints; } bool DbInitializer::updateIndexesAndConstraints() { Q_FOREACH (const TableDescription &table, mSchema->tables()) { // Make sure the foreign key constraints are all there checkForeignKeys(table); checkIndexes(table); } Q_FOREACH (const RelationDescription &relation, mSchema->relations()) { RelationTableDescription relTable(relation); checkForeignKeys(relTable); checkIndexes(relTable); } try { if (!m_pendingIndexes.isEmpty()) { qCDebug(AKONADISERVER_LOG) << "Updating indexes"; execPendingQueries(m_pendingIndexes); m_pendingIndexes.clear(); } if (!m_removedForeignKeys.isEmpty()) { qCDebug(AKONADISERVER_LOG) << "Removing invalid foreign key constraints"; execPendingQueries(m_removedForeignKeys); m_removedForeignKeys.clear(); } if (!m_pendingForeignKeys.isEmpty()) { qCDebug(AKONADISERVER_LOG) << "Adding new foreign key constraints"; execPendingQueries(m_pendingForeignKeys); m_pendingForeignKeys.clear(); } } catch (const DbException &e) { qCDebug(AKONADISERVER_LOG) << "Updating index failed: " << e.what(); return false; } qCDebug(AKONADISERVER_LOG) << "Indexes successfully created"; return true; } void DbInitializer::execPendingQueries(const QStringList &queries) { for (const QString &statement : queries) { qCDebug(AKONADISERVER_LOG) << statement; execQuery(statement); } } QString DbInitializer::sqlType(const ColumnDescription &col, int size) const { Q_UNUSED(size); if (col.type == QLatin1String("int")) { return QStringLiteral("INTEGER"); } if (col.type == QLatin1String("qint64")) { return QStringLiteral("BIGINT"); } if (col.type == QLatin1String("QString")) { return QStringLiteral("TEXT"); } if (col.type == QLatin1String("QByteArray")) { return QStringLiteral("LONGBLOB"); } if (col.type == QLatin1String("QDateTime")) { return QStringLiteral("TIMESTAMP"); } if (col.type == QLatin1String("bool")) { return QStringLiteral("BOOL"); } if (col.isEnum) { return QStringLiteral("TINYINT"); } qCDebug(AKONADISERVER_LOG) << "Invalid type" << col.type; Q_ASSERT(false); return QString(); } QString DbInitializer::sqlValue(const ColumnDescription &col, const QString &value) const { - if (col.type == QLatin1String("QDateTime") && value == QLatin1String("QDateTime::currentDateTime()")) { + if (col.type == QLatin1String("QDateTime") && value == QLatin1String("QDateTime::currentDateTimeUtc()")) { return QStringLiteral("CURRENT_TIMESTAMP"); } else if (col.isEnum) { return QString::number(col.enumValueMap[value]); } return value; } QString DbInitializer::buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const { return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2").arg(tableDescription.name, buildColumnStatement(columnDescription, tableDescription)); } QString DbInitializer::buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const { const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name); QStringList columns; if (indexDescription.sort.isEmpty()) { columns = indexDescription.columns; } else { columns.reserve(indexDescription.columns.count()); std::transform(indexDescription.columns.cbegin(), indexDescription.columns.cend(), std::back_insert_iterator(columns), [&indexDescription](const QString & column) { return QStringLiteral("%1 %2").arg(column, indexDescription.sort); }); } return QStringLiteral("CREATE %1 INDEX %2 ON %3 (%4)") .arg(indexDescription.isUnique ? QStringLiteral("UNIQUE") : QString(), indexName, tableDescription.name, columns.join(QLatin1Char(','))); } QString DbInitializer::buildAddForeignKeyConstraintStatement(const TableDescription &table, const ColumnDescription &column) const { Q_UNUSED(table); Q_UNUSED(column); return QString(); } QString DbInitializer::buildRemoveForeignKeyConstraintStatement(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const { Q_UNUSED(fk); Q_UNUSED(table); return QString(); } QString DbInitializer::buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete) { return QLatin1Literal("ON UPDATE ") + referentialActionToString(onUpdate) + QLatin1Literal(" ON DELETE ") + referentialActionToString(onDelete); } QString DbInitializer::referentialActionToString(ColumnDescription::ReferentialAction action) { switch (action) { case ColumnDescription::Cascade: return QStringLiteral("CASCADE"); case ColumnDescription::Restrict: return QStringLiteral("RESTRICT"); case ColumnDescription::SetNull: return QStringLiteral("SET NULL"); } Q_ASSERT(!"invalid referential action enum!"); return QString(); } QString DbInitializer::buildPrimaryKeyStatement(const TableDescription &table) { QStringList cols; Q_FOREACH (const ColumnDescription &column, table.columns) { if (column.isPrimaryKey) { cols.push_back(column.name); } } return QLatin1Literal("PRIMARY KEY (") + cols.join(QStringLiteral(", ")) + QLatin1Char(')'); } void DbInitializer::execQuery(const QString &queryString) { // if ( Q_UNLIKELY( mTestInterface ) ) { Qt 4.7 has no Q_UNLIKELY yet if (mTestInterface) { mTestInterface->execStatement(queryString); return; } QSqlQuery query(mDatabase); if (!query.exec(queryString)) { throw DbException(query); } } void DbInitializer::setTestInterface(TestInterface *interface) { mTestInterface = interface; } void DbInitializer::setIntrospector(const DbIntrospector::Ptr &introspector) { m_introspector = introspector; }