diff --git a/src/server/handler/akappend.cpp b/src/server/handler/akappend.cpp index 2e8c4f4f2..7c3afef3d 100644 --- a/src/server/handler/akappend.cpp +++ b/src/server/handler/akappend.cpp @@ -1,452 +1,453 @@ /*************************************************************************** * 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(QStringLiteral("Invalid parent collection")); } if (parentCol.isVirtual()) { return failureResponse(QStringLiteral("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::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(QStringLiteral("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(QStringLiteral("Unable to append item tags.")); } } // Handle individual parts qint64 partSizes = 0; PartStreamer streamer(connection(), item); 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); 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::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) { Protocol::FetchItemsResponse resp; resp.setId(item.id()); resp.setMTime(item.datetime()); Handler::sendResponse(std::move(resp)); return true; } Protocol::ItemFetchScope fetchScope; fetchScope.setAncestorDepth(Protocol::ItemFetchScope::ParentAncestor); fetchScope.setFetch(Protocol::ItemFetchScope::AllAttributes | Protocol::ItemFetchScope::FullPayload | Protocol::ItemFetchScope::CacheOnly | Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::GID | Protocol::ItemFetchScope::MTime | Protocol::ItemFetchScope::RemoteID | Protocol::ItemFetchScope::RemoteRevision | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::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(QStringLiteral("Failed to commit transaction")); } storageTrx.commit(); } else { // Merging is always restricted to the same collection SelectQueryBuilder qb; + qb.setForUpdate(); qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, parentCol.id()); Query::Condition rootCondition(Query::Or); Query::Condition mergeCondition(Query::And); if (cmd.mergeModes() & Protocol::CreateItemCommand::GID) { mergeCondition.addValueCondition(PimItem::gidColumn(), Query::Equals, item.gid()); } if (cmd.mergeModes() & Protocol::CreateItemCommand::RemoteID) { mergeCondition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); } rootCondition.addCondition(mergeCondition); // If an Item with matching RID but empty GID exists during GID merge, // merge into this item instead of creating a new one if (cmd.mergeModes() & Protocol::CreateItemCommand::GID && !item.remoteId().isEmpty()) { mergeCondition = Query::Condition(Query::And); mergeCondition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); mergeCondition.addValueCondition(PimItem::gidColumn(), Query::Equals, QStringLiteral("")); rootCondition.addCondition(mergeCondition); } qb.addCondition(rootCondition); 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:"; for (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(QStringLiteral("Multiple merge candidates, aborting")); } } return successResponse(); } diff --git a/src/server/handler/move.cpp b/src/server/handler/move.cpp index 5053935cb..15085d639 100644 --- a/src/server/handler/move.cpp +++ b/src/server/handler/move.cpp @@ -1,168 +1,169 @@ /* 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" #include "akonadiserver_debug.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; + qb.setForUpdate(); ItemQueryHelper::itemSetToQuery(ImapSet(ids), qb); qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::NotEquals, mDestination.id()); if (!qb.exec()) { failureResponse("Unable to execute query"); return; } const QVector items = qb.result(); if (items.isEmpty()) { return; } const QDateTime mtime = QDateTime::currentDateTimeUtc(); // 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; } // 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); } // 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). // We do it after emitting notification so that the FetchHelper can still // retrieve the RID 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; } } 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); QObject::connect(&retriever, &ItemRetriever::itemsRetrieved, [this](const QList &ids) { itemsRetrieved(ids); }); if (!retriever.exec()) { return failureResponse(retriever.lastError()); } return successResponse(); } diff --git a/src/server/handler/store.cpp b/src/server/handler/store.cpp index db981ee64..684468181 100644 --- a/src/server/handler/store.cpp +++ b/src/server/handler/store.cpp @@ -1,385 +1,386 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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 "store.h" #include "connection.h" #include "handlerhelper.h" #include "storage/datastore.h" #include "storage/transaction.h" #include "storage/itemqueryhelper.h" #include "storage/selectquerybuilder.h" #include "storage/parthelper.h" #include "storage/dbconfig.h" #include "storage/itemretriever.h" #include "storage/parttypehelper.h" #include "storage/partstreamer.h" #include #include "akonadiserver_debug.h" #include #include using namespace Akonadi; using namespace Akonadi::Server; static bool payloadChanged(const QSet &changes) { for (const QByteArray &change : changes) { if (change.startsWith(AKONADI_PARAM_PLD)) { return true; } } return false; } bool Store::replaceFlags(const PimItem::List &item, const QSet &flags, bool &flagsChanged) { Flag::List flagList = HandlerHelper::resolveFlags(flags); DataStore *store = connection()->storageBackend(); if (!store->setItemsFlags(item, flagList, &flagsChanged)) { qCDebug(AKONADISERVER_LOG) << "Store::replaceFlags: Unable to replace flags"; return false; } return true; } bool Store::addFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) { const Flag::List flagList = HandlerHelper::resolveFlags(flags); DataStore *store = connection()->storageBackend(); if (!store->appendItemsFlags(items, flagList, &flagsChanged)) { qCDebug(AKONADISERVER_LOG) << "Store::addFlags: Unable to add new item flags"; return false; } return true; } bool Store::deleteFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) { DataStore *store = connection()->storageBackend(); QVector flagList; flagList.reserve(flags.size()); for (auto iter = flags.cbegin(), end = flags.cend(); iter != end; ++iter) { Flag flag = Flag::retrieveByName(QString::fromUtf8(*iter)); if (!flag.isValid()) { continue; } flagList.append(flag); } if (!store->removeItemsFlags(items, flagList, &flagsChanged)) { qCDebug(AKONADISERVER_LOG) << "Store::deleteFlags: Unable to remove item flags"; return false; } return true; } bool Store::replaceTags(const PimItem::List &item, const Scope &tags, bool &tagsChanged) { const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); if (!connection()->storageBackend()->setItemsTags(item, tagList, &tagsChanged)) { qCDebug(AKONADISERVER_LOG) << "Store::replaceTags: unable to replace tags"; return false; } return true; } bool Store::addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged) { const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); if (!connection()->storageBackend()->appendItemsTags(items, tagList, &tagsChanged)) { qCDebug(AKONADISERVER_LOG) << "Store::addTags: Unable to add new item tags"; return false; } return true; } bool Store::deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged) { const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()); if (!connection()->storageBackend()->removeItemsTags(items, tagList, &tagsChanged)) { qCDebug(AKONADISERVER_LOG) << "Store::deleteTags: Unable to remove item tags"; return false; } return true; } bool Store::parseStream() { const auto &cmd = Protocol::cmdCast(m_command); //parseCommand(); DataStore *store = connection()->storageBackend(); Transaction transaction(store, QStringLiteral("STORE")); ExternalPartStorageTransaction storageTrx; // Set the same modification time for each item. QDateTime modificationtime = QDateTime::currentDateTimeUtc(); if (DbType::type(store->database()) != DbType::Sqlite) { // Remove milliseconds from the modificationtime. PSQL and MySQL don't // support milliseconds in DATETIME column, so FETCHed Items will report // time without milliseconds, while this command would return answer // with milliseconds modificationtime = modificationtime.addMSecs(-modificationtime.time().msec()); } // retrieve selected items SelectQueryBuilder qb; + qb.setForUpdate(); ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); if (!qb.exec()) { return failureResponse("Unable to retrieve items"); } PimItem::List pimItems = qb.result(); if (pimItems.isEmpty()) { return failureResponse("No items found"); } for (int i = 0; i < pimItems.size(); ++i) { if (cmd.oldRevision() > -1) { // check for conflicts if a resources tries to overwrite an item with dirty payload const PimItem &pimItem = pimItems.at(i); if (connection()->isOwnerResource(pimItem)) { if (pimItem.dirty()) { const QString error = QStringLiteral("[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE."); return failureResponse( error.arg(pimItem.collection().resource().name()) .arg(pimItem.id()) .arg(pimItem.remoteId()).arg(pimItem.collectionId())); } } // check and update revisions if (pimItems.at(i).rev() != (int) cmd.oldRevision()) { const QString error = QStringLiteral("[LLCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with revision %5; the item was modified elsewhere and has revision %6, aborting STORE."); return failureResponse(error.arg(pimItem.collection().resource().name()) .arg(pimItem.id()) .arg(pimItem.remoteId()).arg(pimItem.collectionId()) .arg(cmd.oldRevision()).arg(pimItems.at(i).rev())); } } } PimItem &item = pimItems.first(); QSet changes; qint64 partSizes = 0; qint64 size = 0; bool flagsChanged = false; bool tagsChanged = false; if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedFlags) { if (!addFlags(pimItems, cmd.addedFlags(), flagsChanged)) { return failureResponse("Unable to add item flags"); } } if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedFlags) { if (!deleteFlags(pimItems, cmd.removedFlags(), flagsChanged)) { return failureResponse("Unable to remove item flags"); } } if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Flags) { if (!replaceFlags(pimItems, cmd.flags(), flagsChanged)) { return failureResponse("Unable to reset flags"); } } if (flagsChanged) { changes << AKONADI_PARAM_FLAGS; } if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedTags) { if (!addTags(pimItems, cmd.addedTags(), tagsChanged)) { return failureResponse("Unable to add item tags"); } } if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedTags) { if (!deleteTags(pimItems, cmd.removedTags(), tagsChanged)) { return failureResponse("Unabel to remove item tags"); } } if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Tags) { if (!replaceTags(pimItems, cmd.tags(), tagsChanged)) { return failureResponse("Unable to reset item tags"); } } if (tagsChanged) { changes << AKONADI_PARAM_TAGS; } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteID) { if (item.remoteId() != cmd.remoteId()) { if (!connection()->isOwnerResource(item)) { return failureResponse("Only resources can modify remote identifiers"); } item.setRemoteId(cmd.remoteId()); changes << AKONADI_PARAM_REMOTEID; } } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::GID) { if (item.gid() != cmd.gid()) { item.setGid(cmd.gid()); } changes << AKONADI_PARAM_GID; } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteRevision) { if (item.remoteRevision() != cmd.remoteRevision()) { if (!connection()->isOwnerResource(item)) { return failureResponse("Only resources can modify remote revisions"); } item.setRemoteRevision(cmd.remoteRevision()); changes << AKONADI_PARAM_REMOTEREVISION; } } if (item.isValid() && !cmd.dirty()) { item.setDirty(false); } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Size) { size = cmd.itemSize(); changes << AKONADI_PARAM_SIZE; } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedParts) { if (!cmd.removedParts().isEmpty()) { if (!store->removeItemParts(item, cmd.removedParts())) { return failureResponse("Unable to remove item parts"); } Q_FOREACH (const QByteArray &part, cmd.removedParts()) { changes.insert(part); } } } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Parts) { PartStreamer streamer(connection(), item); Q_FOREACH (const QByteArray &partName, cmd.parts()) { qint64 partSize = 0; if (!streamer.stream(true, partName, partSize)) { return failureResponse(streamer.error()); } changes.insert(partName); partSizes += partSize; } } if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Attributes) { PartStreamer streamer(connection(), item); const Protocol::Attributes attrs = cmd.attributes(); for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { bool changed = false; if (!streamer.streamAttribute(true, iter.key(), iter.value(), &changed)) { return failureResponse(streamer.error()); } if (changed) { changes.insert(iter.key()); } } } QDateTime datetime; if (!changes.isEmpty() || cmd.invalidateCache() || !cmd.dirty()) { // update item size if (pimItems.size() == 1 && (size > 0 || partSizes > 0)) { pimItems.first().setSize(qMax(size, partSizes)); } const bool onlyRemoteIdChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEID)); const bool onlyRemoteRevisionChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEREVISION)); const bool onlyRemoteIdAndRevisionChanged = (changes.size() == 2 && changes.contains(AKONADI_PARAM_REMOTEID) && changes.contains(AKONADI_PARAM_REMOTEREVISION)); const bool onlyFlagsChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_FLAGS)); const bool onlyGIDChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_GID)); // If only the remote id and/or the remote revision changed, we don't have to increase the REV, // because these updates do not change the payload and can only be done by the owning resource -> no conflicts possible const bool revisionNeedsUpdate = (!changes.isEmpty() && !onlyRemoteIdChanged && !onlyRemoteRevisionChanged && !onlyRemoteIdAndRevisionChanged && !onlyGIDChanged); // run update query and prepare change notifications for (int i = 0; i < pimItems.count(); ++i) { if (revisionNeedsUpdate) { pimItems[i].setRev(pimItems[i].rev() + 1); } PimItem &item = pimItems[i]; item.setDatetime(modificationtime); item.setAtime(modificationtime); if (!connection()->isOwnerResource(item) && payloadChanged(changes)) { item.setDirty(true); } if (!item.update()) { return failureResponse("Unable to write item changes into the database"); } if (cmd.invalidateCache()) { if (!store->invalidateItemCache(item)) { return failureResponse("Unable to invalidate item cache in the database"); } } // flags change notification went separatly during command parsing // GID-only changes are ignored to prevent resources from updating their storage when no actual change happened if (cmd.notify() && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged) { // Don't send FLAGS notification in itemChanged changes.remove(AKONADI_PARAM_FLAGS); store->notificationCollector()->itemChanged(item, changes); } if (!cmd.noResponse()) { Protocol::ModifyItemsResponse resp; resp.setId(item.id()); resp.setNewRevision(item.rev()); sendResponse(std::move(resp)); } } if (!transaction.commit()) { return failureResponse("Cannot commit transaction."); } // Always commit storage changes (deletion) after DB transaction storageTrx.commit(); datetime = modificationtime; } else { datetime = pimItems.first().datetime(); } Protocol::ModifyItemsResponse resp; resp.setModificationDateTime(datetime); return successResponse(std::move(resp)); } diff --git a/src/server/storage/querybuilder.cpp b/src/server/storage/querybuilder.cpp index f0dceb931..00eae6dad 100644 --- a/src/server/storage/querybuilder.cpp +++ b/src/server/storage/querybuilder.cpp @@ -1,619 +1,633 @@ /* Copyright (c) 2007 - 2012 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 "querybuilder.h" #include "akonadiserver_debug.h" #ifndef QUERYBUILDER_UNITTEST #include "storage/datastore.h" #include "storage/querycache.h" #include "storage/storagedebugger.h" #endif #include #include #include using namespace Akonadi::Server; static QLatin1String compareOperatorToString(Query::CompareOperator op) { switch (op) { case Query::Equals: return QLatin1String(" = "); case Query::NotEquals: return QLatin1String(" <> "); case Query::Is: return QLatin1String(" IS "); case Query::IsNot: return QLatin1String(" IS NOT "); case Query::Less: return QLatin1String(" < "); case Query::LessOrEqual: return QLatin1String(" <= "); case Query::Greater: return QLatin1String(" > "); case Query::GreaterOrEqual: return QLatin1String(" >= "); case Query::In: return QLatin1String(" IN "); case Query::NotIn: return QLatin1String(" NOT IN "); case Query::Like: return QLatin1String(" LIKE "); } Q_ASSERT_X(false, "QueryBuilder::compareOperatorToString()", "Unknown compare operator."); return QLatin1String(""); } static QLatin1String logicOperatorToString(Query::LogicOperator op) { switch (op) { case Query::And: return QLatin1String(" AND "); case Query::Or: return QLatin1String(" OR "); } Q_ASSERT_X(false, "QueryBuilder::logicOperatorToString()", "Unknown logic operator."); return QLatin1String(""); } static QLatin1String sortOrderToString(Query::SortOrder order) { switch (order) { case Query::Ascending: return QLatin1String(" ASC"); case Query::Descending: return QLatin1String(" DESC"); } Q_ASSERT_X(false, "QueryBuilder::sortOrderToString()", "Unknown sort order."); return QLatin1String(""); } static void appendJoined(QString *statement, const QStringList &strings, const QLatin1String &glue = QLatin1String(", ")) { for (int i = 0, c = strings.size(); i < c; ++i) { *statement += strings.at(i); if (i + 1 < c) { *statement += glue; } } } QueryBuilder::QueryBuilder(const QString &table, QueryBuilder::QueryType type) : mTable(table) #ifndef QUERYBUILDER_UNITTEST , mDatabaseType(DbType::type(DataStore::self()->database())) , mQuery(DataStore::self()->database()) #else , mDatabaseType(DbType::Unknown) #endif , mType(type) , mIdentificationColumn() , mLimit(-1) , mDistinct(false) { static const QString defaultIdColumn = QStringLiteral("id"); mIdentificationColumn = defaultIdColumn; } void QueryBuilder::setDatabaseType(DbType::Type type) { mDatabaseType = type; } void QueryBuilder::addJoin(JoinType joinType, const QString &table, const Query::Condition &condition) { Q_ASSERT((joinType == InnerJoin && (mType == Select || mType == Update)) || (joinType == LeftJoin && mType == Select)); if (mJoinedTables.contains(table)) { // InnerJoin is more restrictive than a LeftJoin, hence use that in doubt mJoins[table].first = qMin(joinType, mJoins.value(table).first); mJoins[table].second.addCondition(condition); } else { mJoins[table] = qMakePair(joinType, condition); mJoinedTables << table; } } void QueryBuilder::addJoin(JoinType joinType, const QString &table, const QString &col1, const QString &col2) { Query::Condition condition; condition.addColumnCondition(col1, Query::Equals, col2); addJoin(joinType, table, condition); } void QueryBuilder::addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].addValueCondition(column, op, value); } void QueryBuilder::addColumnCondition(const QString &column, Query::CompareOperator op, const QString &column2, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].addColumnCondition(column, op, column2); } QSqlQuery &QueryBuilder::query() { return mQuery; } void QueryBuilder::sqliteAdaptUpdateJoin(Query::Condition &condition) { // FIXME: This does not cover all cases by far. It however can handle most // (probably all) of the update-join queries we do in Akonadi and convert them // properly into a SQLite-compatible query. Better than nothing ;-) if (!condition.mSubConditions.isEmpty()) { for (int i = condition.mSubConditions.count() - 1; i >= 0; --i) { sqliteAdaptUpdateJoin(condition.mSubConditions[i]); } return; } QString table; if (condition.mColumn.contains(QLatin1Char('.'))) { table = condition.mColumn.left(condition.mColumn.indexOf(QLatin1Char('.'))); } else { return; } if (!mJoinedTables.contains(table)) { return; } const QPair joinCondition = mJoins.value(table); QueryBuilder qb(table, Select); qb.addColumn(condition.mColumn); qb.addCondition(joinCondition.second); // Convert the subquery to string condition.mColumn.reserve(1024); condition.mColumn.resize(0); condition.mColumn += QLatin1String("( "); qb.buildQuery(&condition.mColumn); condition.mColumn += QLatin1String(" )"); } void QueryBuilder::buildQuery(QString *statement) { // we add the ON conditions of Inner Joins in a Update query here // but don't want to change the mRootCondition on each exec(). Query::Condition whereCondition = mRootCondition[WhereCondition]; switch (mType) { case Select: // Enable forward-only on all SELECT queries, since we never need to // iterate backwards. This is a memory optimization. mQuery.setForwardOnly(true); *statement += QLatin1String("SELECT "); if (mDistinct) { *statement += QLatin1String("DISTINCT "); } Q_ASSERT_X(mColumns.count() > 0, "QueryBuilder::exec()", "No columns specified"); appendJoined(statement, mColumns); *statement += QLatin1String(" FROM "); *statement += mTable; for (const QString &joinedTable : qAsConst(mJoinedTables)) { const QPair &join = mJoins.value(joinedTable); switch (join.first) { case LeftJoin: *statement += QLatin1String(" LEFT JOIN "); break; case InnerJoin: *statement += QLatin1String(" INNER JOIN "); break; } *statement += joinedTable; *statement += QLatin1String(" ON "); buildWhereCondition(statement, join.second); } break; case Insert: { *statement += QLatin1String("INSERT INTO "); *statement += mTable; *statement += QLatin1String(" ("); for (int i = 0, c = mColumnValues.size(); i < c; ++i) { *statement += mColumnValues.at(i).first; if (i + 1 < c) { *statement += QLatin1String(", "); } } *statement += QLatin1String(") VALUES ("); for (int i = 0, c = mColumnValues.size(); i < c; ++i) { bindValue(statement, mColumnValues.at(i).second); if (i + 1 < c) { *statement += QLatin1String(", "); } } *statement += QLatin1Char(')'); if (mDatabaseType == DbType::PostgreSQL && !mIdentificationColumn.isEmpty()) { *statement += QLatin1String(" RETURNING ") + mIdentificationColumn; } break; } case Update: { // put the ON condition into the WHERE part of the UPDATE query if (mDatabaseType != DbType::Sqlite) { for (const QString &table : qAsConst(mJoinedTables)) { const QPair< JoinType, Query::Condition > &join = mJoins.value(table); Q_ASSERT(join.first == InnerJoin); whereCondition.addCondition(join.second); } } else { // Note: this will modify the whereCondition sqliteAdaptUpdateJoin(whereCondition); } *statement += QLatin1String("UPDATE "); *statement += mTable; if (mDatabaseType == DbType::MySQL && !mJoinedTables.isEmpty()) { // for mysql we list all tables directly *statement += QLatin1String(", "); appendJoined(statement, mJoinedTables); } *statement += QLatin1String(" SET "); Q_ASSERT_X(mColumnValues.count() >= 1, "QueryBuilder::exec()", "At least one column needs to be changed"); for (int i = 0, c = mColumnValues.size(); i < c; ++i) { const QPair &p = mColumnValues.at(i); *statement += p.first; *statement += QLatin1String(" = "); bindValue(statement, p.second); if (i + 1 < c) { *statement += QLatin1String(", "); } } if (mDatabaseType == DbType::PostgreSQL && !mJoinedTables.isEmpty()) { // PSQL have this syntax // FROM t1 JOIN t2 JOIN ... *statement += QLatin1String(" FROM "); appendJoined(statement, mJoinedTables, QLatin1String(" JOIN ")); } break; } case Delete: *statement += QLatin1String("DELETE FROM "); *statement += mTable; break; default: Q_ASSERT_X(false, "QueryBuilder::exec()", "Unknown enum value"); } if (!whereCondition.isEmpty()) { *statement += QLatin1String(" WHERE "); buildWhereCondition(statement, whereCondition); } if (!mGroupColumns.isEmpty()) { *statement += QLatin1String(" GROUP BY "); appendJoined(statement, mGroupColumns); } if (!mRootCondition[HavingCondition].isEmpty()) { *statement += QLatin1String(" HAVING "); buildWhereCondition(statement, mRootCondition[HavingCondition]); } if (!mSortColumns.isEmpty()) { Q_ASSERT_X(mType == Select, "QueryBuilder::exec()", "Order statements are only valid for SELECT queries"); *statement += QLatin1String(" ORDER BY "); for (int i = 0, c = mSortColumns.size(); i < c; ++i) { const QPair &order = mSortColumns.at(i); *statement += order.first; *statement += sortOrderToString(order.second); if (i + 1 < c) { *statement += QLatin1String(", "); } } } if (mLimit > 0) { *statement += QLatin1Literal(" LIMIT ") + QString::number(mLimit); } + + if (mType == Select && mForUpdate) { + if (mDatabaseType == DbType::Sqlite) { + // SQLite does not support SELECT ... FOR UPDATE syntax, because it does + // table-level locking + } else { + *statement += QLatin1Literal(" FOR UPDATE"); + } + } } bool QueryBuilder::retryLastTransaction(bool rollback) { #ifndef QUERYBUILDER_UNITTEST mQuery = DataStore::self()->retryLastTransaction(rollback); return !mQuery.lastError().isValid(); #else Q_UNUSED(rollback); return true; #endif } bool QueryBuilder::exec() { QString statement; statement.reserve(1024); buildQuery(&statement); #ifndef QUERYBUILDER_UNITTEST if (QueryCache::contains(statement)) { mQuery = QueryCache::query(statement); } else { mQuery.clear(); mQuery.prepare(statement); QueryCache::insert(statement, mQuery); } //too heavy debug info but worths to have from time to time //qCDebug(AKONADISERVER_LOG) << "Executing query" << statement; bool isBatch = false; for (int i = 0; i < mBindValues.count(); ++i) { mQuery.bindValue(QLatin1Char(':') + QString::number(i), mBindValues[i]); if (!isBatch && static_cast(mBindValues[i].type()) == QMetaType::QVariantList) { isBatch = true; } //qCDebug(AKONADISERVER_LOG) << QString::fromLatin1( ":%1" ).arg( i ) << mBindValues[i]; } bool ret; if (StorageDebugger::instance()->isSQLDebuggingEnabled()) { QTime t; t.start(); if (isBatch) { ret = mQuery.execBatch(); } else { ret = mQuery.exec(); } StorageDebugger::instance()->queryExecuted(reinterpret_cast(DataStore::self()), mQuery, t.elapsed()); } else { StorageDebugger::instance()->incSequence(); if (isBatch) { ret = mQuery.execBatch(); } else { ret = mQuery.exec(); } } // Add the query to DataStore so that we can replay it in case transaction deadlocks. // The method does nothing when this query is not executed within a transaction. // We don't care whether the query was successful or not. In case of error, the caller // will rollback the transaction anyway, and all cached queries will be removed. DataStore::self()->addQueryToTransaction(statement, mBindValues, isBatch); if (!ret) { // Handle transaction deadlocks and timeouts by attempting to replay the transaction. if (mDatabaseType == DbType::PostgreSQL) { const QString dbError = mQuery.lastError().databaseText(); if (dbError.contains(QLatin1String("40P01" /* deadlock_detected */))) { qCDebug(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; qCDebug(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(); } } else if (mDatabaseType == DbType::MySQL) { const QString lastErrorStr = mQuery.lastError().nativeErrorCode(); const int error = lastErrorStr.isEmpty() ? -1 : lastErrorStr.toInt(); if (error == 1213 /* ER_LOCK_DEADLOCK */) { qCDebug(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; qCDebug(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(); } else if (error == 1205 /* ER_LOCK_WAIT_TIMEOUT */) { qCDebug(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; qCDebug(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(); } } else if (mDatabaseType == DbType::Sqlite && !DbType::isSystemSQLite(DataStore::self()->database())) { const QString lastErrorStr = mQuery.lastError().nativeErrorCode(); const int error = lastErrorStr.isEmpty() ? -1 : lastErrorStr.toInt(); if (error == 6 /* SQLITE_LOCKED */) { qCDebug(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; qCDebug(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(true); } else if (error == 5 /* SQLITE_BUSY */) { qCDebug(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; qCDebug(AKONADISERVER_LOG) << mQuery.lastError().text(); return retryLastTransaction(true); } } else if (mDatabaseType == DbType::Sqlite) { // We can't have a transaction deadlock in SQLite when using driver shipped // with Qt, because it does not support concurrent transactions and DataStore // serializes them through a global lock. } qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR:"; qCCritical(AKONADISERVER_LOG) << " Error code:" << mQuery.lastError().nativeErrorCode(); qCCritical(AKONADISERVER_LOG) << " DB error: " << mQuery.lastError().databaseText(); qCCritical(AKONADISERVER_LOG) << " Error text:" << mQuery.lastError().text(); qCCritical(AKONADISERVER_LOG) << " Values:" << mQuery.boundValues(); qCCritical(AKONADISERVER_LOG) << " Query:" << statement; return false; } #else mStatement = statement; #endif return true; } void QueryBuilder::addColumns(const QStringList &cols) { mColumns << cols; } void QueryBuilder::addColumn(const QString &col) { mColumns << col; } void QueryBuilder::addColumn(const Query::Case &caseStmt) { QString query; buildCaseStatement(&query, caseStmt); mColumns.append(query); } void QueryBuilder::addAggregation(const QString &col, const QString &aggregate) { mColumns.append(aggregate + QLatin1Char('(') + col + QLatin1Char(')')); } void QueryBuilder::addAggregation(const Query::Case &caseStmt, const QString &aggregate) { QString query(aggregate + QLatin1Char('(')); buildCaseStatement(&query, caseStmt); query += QLatin1Char(')'); mColumns.append(query); } void QueryBuilder::bindValue(QString *query, const QVariant &value) { mBindValues << value; *query += QLatin1Char(':') + QString::number(mBindValues.count() - 1); } void QueryBuilder::buildWhereCondition(QString *query, const Query::Condition &cond) { if (!cond.isEmpty()) { *query += QLatin1String("( "); const QLatin1String glue = logicOperatorToString(cond.mCombineOp); const Query::Condition::List &subConditions = cond.subConditions(); for (int i = 0, c = subConditions.size(); i < c; ++i) { buildWhereCondition(query, subConditions.at(i)); if (i + 1 < c) { *query += glue; } } *query += QLatin1String(" )"); } else { *query += cond.mColumn; *query += compareOperatorToString(cond.mCompareOp); if (cond.mComparedColumn.isEmpty()) { if (cond.mComparedValue.isValid()) { if (cond.mComparedValue.canConvert(QVariant::List)) { *query += QLatin1String("( "); const QVariantList &entries = cond.mComparedValue.toList(); Q_ASSERT_X(!entries.isEmpty(), "QueryBuilder::buildWhereCondition()", "No values given for IN condition."); for (int i = 0, c = entries.size(); i < c; ++i) { bindValue(query, entries.at(i)); if (i + 1 < c) { *query += QLatin1String(", "); } } *query += QLatin1String(" )"); } else { bindValue(query, cond.mComparedValue); } } else { *query += QLatin1String("NULL"); } } else { *query += cond.mComparedColumn; } } } void QueryBuilder::buildCaseStatement(QString *query, const Query::Case &caseStmt) { *query += QLatin1String("CASE "); Q_FOREACH (const auto &whenThen, caseStmt.mWhenThen) { *query += QLatin1String("WHEN "); buildWhereCondition(query, whenThen.first); // When *query += QLatin1String(" THEN ") + whenThen.second; // then } if (!caseStmt.mElse.isEmpty()) { *query += QLatin1String(" ELSE ") + caseStmt.mElse; } *query += QLatin1String(" END"); } void QueryBuilder::setSubQueryMode(Query::LogicOperator op, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].setSubQueryMode(op); } void QueryBuilder::addCondition(const Query::Condition &condition, ConditionType type) { Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); mRootCondition[type].addCondition(condition); } void QueryBuilder::addSortColumn(const QString &column, Query::SortOrder order) { mSortColumns << qMakePair(column, order); } void QueryBuilder::addGroupColumn(const QString &column) { Q_ASSERT(mType == Select); mGroupColumns << column; } void QueryBuilder::addGroupColumns(const QStringList &columns) { Q_ASSERT(mType == Select); mGroupColumns += columns; } void QueryBuilder::setColumnValue(const QString &column, const QVariant &value) { mColumnValues << qMakePair(column, value); } void QueryBuilder::setDistinct(bool distinct) { mDistinct = distinct; } void QueryBuilder::setLimit(int limit) { mLimit = limit; } void QueryBuilder::setIdentificationColumn(const QString &column) { mIdentificationColumn = column; } qint64 QueryBuilder::insertId() { if (mDatabaseType == DbType::PostgreSQL) { query().next(); if (mIdentificationColumn.isEmpty()) { return 0; // FIXME: Does this make sense? } return query().record().value(mIdentificationColumn).toLongLong(); } else { const QVariant v = query().lastInsertId(); if (!v.isValid()) { return -1; } bool ok; const qint64 insertId = v.toLongLong(&ok); if (!ok) { return -1; } return insertId; } return -1; } + +void QueryBuilder::setForUpdate(bool forUpdate) +{ + mForUpdate = forUpdate; +} diff --git a/src/server/storage/querybuilder.h b/src/server/storage/querybuilder.h index 687ef4c11..2d86b2095 100644 --- a/src/server/storage/querybuilder.h +++ b/src/server/storage/querybuilder.h @@ -1,295 +1,304 @@ /* Copyright (c) 2007 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. */ #ifndef AKONADI_QUERYBUILDER_H #define AKONADI_QUERYBUILDER_H #include "query.h" #include "dbtype.h" #include #include #include #include #include #include #ifdef QUERYBUILDER_UNITTEST class QueryBuilderTest; #endif namespace Akonadi { namespace Server { /** Helper class to construct arbitrary SQL queries. */ class QueryBuilder { public: enum QueryType { Select, Insert, Update, Delete }; /** * When the same table gets joined as both, Inner- and LeftJoin, * it will be merged into a single InnerJoin since it is more * restrictive. */ enum JoinType { ///NOTE: only supported for UPDATE and SELECT queries. InnerJoin, ///NOTE: only supported for SELECT queries LeftJoin }; /** * Defines the place at which a condition should be evaluated. */ enum ConditionType { /// add condition to WHERE part of the query WhereCondition, /// add condition to HAVING part of the query /// NOTE: only supported for SELECT queries HavingCondition, NUM_CONDITIONS }; /** Creates a new query builder. @param table The main table to operate on. */ explicit QueryBuilder(const QString &table, QueryType type = Select); /** Sets the database which should execute the query. Unfortunately the SQL "standard" is not interpreted in the same way everywhere... */ void setDatabaseType(DbType::Type type); /** Join a table to the query. NOTE: make sure the @c JoinType is supported by the current @c QueryType @param joinType The type of JOIN you want to add. @param table The table to join. @param condition the ON condition for this join. */ void addJoin(JoinType joinType, const QString &table, const Query::Condition &condition); /** Join a table to the query. This is a convenience method to create simple joins like e.g. 'LEFT JOIN t ON c1 = c2'. NOTE: make sure the @c JoinType is supported by the current @c QueryType @param joinType The type of JOIN you want to add. @param table The table to join. @param col1 The first column for the ON statement. @param col2 The second column for the ON statement. */ void addJoin(JoinType joinType, const QString &table, const QString &col1, const QString &col2); /** Adds the given columns to a select query. @param cols The columns you want to select. */ void addColumns(const QStringList &cols); /** Adds the given column to a select query. @param col The column to add. */ void addColumn(const QString &col); /** * Adds the given case statement to a select query. * @param caseStmt The case statement to add. */ void addColumn(const Query::Case &caseStmt); /** * Adds an aggregation statement. * @param col The column to aggregate on * @param aggregate The aggregation function. */ void addAggregation(const QString &col, const QString &aggregate); /** * Adds and aggregation statement with CASE * @param caseStmt The case statement to aggregate on * @param aggregate The aggregation function. */ void addAggregation(const Query::Case &caseStmt, const QString &aggregate); /** Add a WHERE or HAVING condition which compares a column with a given value. @param column The column that should be compared. @param op The operator used for comparison @param value The value @p column is compared to. @param type Defines whether this condition should be part of the WHERE or the HAVING part of the query. Defaults to WHERE. */ 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 another column. @param column The column that should be compared. @param op The operator used for comparison. @param column2 The column @p column is compared to. @param type Defines whether this condition should be part of the WHERE or the HAVING part of the query. Defaults to WHERE. */ void addColumnCondition(const QString &column, Query::CompareOperator op, const QString &column2, ConditionType type = WhereCondition); /** Add a WHERE condition. Use this to build hierarchical conditions. @param condition The condition that the resultset should satisfy. @param type Defines whether this condition should be part of the WHERE or the HAVING part of the query. Defaults to WHERE. */ void addCondition(const Query::Condition &condition, ConditionType type = WhereCondition); /** Define how WHERE or HAVING conditions are combined. @todo Give this method a better name. @param op The logical operator that should be used to combine the conditions. @param type Defines whether the operator should be used for WHERE or for HAVING conditions. Defaults to WHERE conditions. */ void setSubQueryMode(Query::LogicOperator op, ConditionType type = WhereCondition); /** Add sort column. @param column Name of the column to sort. @param order Sort order */ void addSortColumn(const QString &column, Query::SortOrder order = Query::Ascending); /** Add a GROUP BY column. NOTE: Only supported in SELECT queries. @param column Name of the column to use for grouping. */ void addGroupColumn(const QString &column); /** Add list of columns to GROUP BY. NOTE: Only supported in SELECT queries. @param columns Names of columns to use for grouping. */ void addGroupColumns(const QStringList &columns); /** Sets a column to the given value (only valid for INSERT and UPDATE queries). @param column Column to change. @param value The value @p column should be set to. */ void setColumnValue(const QString &column, const QVariant &value); /** * Specify whether duplicates should be included in the result. * @param distinct @c true to remove duplicates, @c false is the default */ void setDistinct(bool distinct); /** * Limits the amount of retrieved rows. * @param limit the maximum number of rows to retrieve. * @note This has no effect on anything but SELECT queries. */ void setLimit(int limit); /** * Sets the column used for identification in an INSERT statement. * The default is "id", only change this on tables without such a column * (usually n:m helper tables). * @param column Name of the identification column, empty string to disable this. * @note This only affects PostgreSQL. * @see insertId() */ void setIdentificationColumn(const QString &column); /** Returns the query, only valid after exec(). */ QSqlQuery &query(); /** Executes the query, returns true on success. */ bool exec(); /** Returns the ID of the newly created record (only valid for INSERT queries) @note This will assert when being used with setIdentificationColumn() called with an empty string. @returns -1 if invalid */ qint64 insertId(); + /** + Indicate to the database to acquire an exclusive lock on the rows already during + SELECT statement. + + Only makes sense in SELECT queries. + */ + void setForUpdate(bool forUpdate = true); + private: void buildQuery(QString *query); void bindValue(QString *query, const QVariant &value); void buildWhereCondition(QString *query, const Query::Condition &cond); void buildCaseStatement(QString *query, const Query::Case &caseStmt); /** * SQLite does not support JOINs with UPDATE, so we have to convert it into * subqueries */ void sqliteAdaptUpdateJoin(Query::Condition &cond); bool retryLastTransaction(bool rollback = false); private: QString mTable; DbType::Type mDatabaseType; Query::Condition mRootCondition[NUM_CONDITIONS]; QSqlQuery mQuery; QueryType mType; QStringList mColumns; QVector mBindValues; QVector > mSortColumns; QStringList mGroupColumns; QVector > mColumnValues; QString mIdentificationColumn; // we must make sure that the tables are joined in the correct order // QMap sorts by key which might invalidate the queries QStringList mJoinedTables; QMap< QString, QPair< JoinType, Query::Condition > > mJoins; int mLimit; bool mDistinct; + bool mForUpdate = false; #ifdef QUERYBUILDER_UNITTEST QString mStatement; friend class ::QueryBuilderTest; #endif }; } // namespace Server } // namespace Akonadi #endif diff --git a/templates/akonadiresource/CMakeLists.txt b/templates/akonadiresource/CMakeLists.txt index 4608ebb33..ea43225d6 100644 --- a/templates/akonadiresource/CMakeLists.txt +++ b/templates/akonadiresource/CMakeLists.txt @@ -1,33 +1,33 @@ cmake_minimum_required(VERSION 3.1) project(%{APPNAMELC}) set(KF5_MIN_VERSION "5.38.0") set(ECM_MIN_VERSION ${KF5_MIN_VERSION}) find_package(ECM ${ECM_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH} ${CMAKE_MODULE_PATH}) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMQtDeclareLoggingCategory) -set(QT_MIN_VERSION "5.9.0") +set(QT_MIN_VERSION "5.9.1") find_package(Qt5 ${QT_MIN_VERSION} REQUIRED Core DBus Gui) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) set(AKONADI_MIN_VERSION "5.2") find_package(KF5Akonadi ${AKONADI_MIN_VERSION} CONFIG REQUIRED) find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable") if (NOT XSLTPROC_EXECUTABLE) message(FATAL_ERROR "\nThe command line XSLT processor program 'xsltproc' could not be found.\nPlease install xsltproc.\n") endif() add_subdirectory(src) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/templates/akonadiserializer/CMakeLists.txt b/templates/akonadiserializer/CMakeLists.txt index b6f278c70..6133ac11d 100644 --- a/templates/akonadiserializer/CMakeLists.txt +++ b/templates/akonadiserializer/CMakeLists.txt @@ -1,27 +1,27 @@ cmake_minimum_required(VERSION 3.1) project(%{APPNAMELC}) set(KF5_MIN_VERSION "5.38.0") set(ECM_MIN_VERSION ${KF5_MIN_VERSION}) find_package(ECM ${ECM_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_MODULE_PATH}) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) -set(QT_MIN_VERSION "5.9.0") +set(QT_MIN_VERSION "5.9.1") find_package(Qt5 ${QT_MIN_VERSION} REQUIRED Core Network Gui) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) set(AKONADI_MIN_VERSION "5.2") find_package(KF5Akonadi ${AKONADI_MIN_VERSION} CONFIG REQUIRED) add_subdirectory(src) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)